Add Atlas Cloud LLM provider (#388)

* Add Atlas Cloud LLM provider

* Add Atlas Cloud onboarding support

* fix(validators): update atlascloud key validation to handle insufficient balance case

---------

Co-authored-by: binyangzhu000-sudo <224954946+binyangzhu000-sudo@users.noreply.github.com>
Co-authored-by: X-iZhang <zacharyzhang2022@gmail.com>
This commit is contained in:
nb213
2026-07-29 21:09:44 +08:00
committed by GitHub
parent 562ce0eb83
commit 4ddf7ebe52
12 changed files with 159 additions and 4 deletions
+2 -1
View File
@@ -18,7 +18,8 @@ KIMI_API_KEY= # kimi.com/code (Kimi 代码计划)
# Aggregator platforms (optional)
SILICONFLOW_API_KEY= # siliconflow.cn
OPENROUTER_API_KEY= # openrouter.ai
REQUESTY_API_KEY= # requesty.ai — get a key at app.requesty.ai/api-keys (docs: docs.requesty.ai)
REQUESTY_API_KEY= # requesty.ai
ATLASCLOUD_API_KEY= # atlascloud.ai
# Custom endpoints (optional)
CUSTOM_OPENAI_API_KEY= # OpenAI-compatible endpoint
+1
View File
@@ -30,6 +30,7 @@ VALID_PROVIDERS: frozenset[str] = frozenset(
"nvidia",
"siliconflow",
"openrouter",
"atlascloud",
"requesty",
"custom-openai",
"custom-anthropic",
+6
View File
@@ -15,6 +15,7 @@ from ..settings import EvoScientistConfig
from .style import QMARK, WIZARD_STYLE, console
from .validators import (
validate_anthropic_key,
validate_atlascloud_key,
validate_dashscope_code_key,
validate_dashscope_key,
validate_deepseek_key,
@@ -71,6 +72,11 @@ def _provider_key_info(config: EvoScientistConfig, provider: str):
config.openrouter_api_key or os.environ.get("OPENROUTER_API_KEY", ""),
validate_openrouter_key,
),
"atlascloud": (
"Atlas Cloud",
config.atlascloud_api_key or os.environ.get("ATLASCLOUD_API_KEY", ""),
validate_atlascloud_key,
),
"requesty": (
"Requesty",
config.requesty_api_key or os.environ.get("REQUESTY_API_KEY", ""),
+4
View File
@@ -296,6 +296,10 @@ def _step_provider(
title="OpenRouter (aggregator — Grok, Gemini, Qwen, etc.)",
value="openrouter",
),
Choice(
title="Atlas Cloud (aggregator — DeepSeek, Qwen, etc.)",
value="atlascloud",
),
Choice(
title="Requesty (aggregator — OpenAI, Anthropic, Gemini, xAI, etc.)",
value="requesty",
+43
View File
@@ -321,6 +321,49 @@ def validate_openrouter_key(api_key: str) -> tuple[bool, str]:
return False, f"Error: {e}"
def validate_atlascloud_key(api_key: str) -> tuple[bool, str]:
"""Validate an Atlas Cloud key with a nonexistent sentinel model.
The probe deliberately targets a nonexistent sentinel model. A 404 means
authentication passed and model resolution failed; 200 also confirms
authentication if the sentinel unexpectedly resolves. A 401/403 means the
key was rejected. Other statuses remain inconclusive until verified.
"""
if not api_key:
return True, "Skipped (no key provided)"
try:
import httpx
resp = httpx.post(
"https://api.atlascloud.ai/v1/chat/completions",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json={
"model": "atlascloud/auth-preflight",
"messages": [{"role": "user", "content": "ping"}],
"max_tokens": 1,
},
timeout=10,
)
if resp.status_code in (200, 404):
return True, "Valid"
# Atlas checks account balance before model resolution: a valid key
# on an uncredited account gets 402 from the sentinel probe.
if resp.status_code == 402:
return True, "Valid (insufficient balance — top up to use)"
if resp.status_code in (401, 403):
return False, "Invalid API key"
return False, f"Validation inconclusive (HTTP {resp.status_code})"
except Exception as e:
classified = _classify_validation_error(e)
if classified is not None:
return classified
return False, f"Error: {e}"
def validate_requesty_key(api_key: str) -> tuple[bool, str]:
"""Validate a Requesty API key against the router's auth layer.
+1
View File
@@ -118,6 +118,7 @@ _PROVIDER_KEY_ATTR = {
"google-genai": "google_api_key",
"siliconflow": "siliconflow_api_key",
"openrouter": "openrouter_api_key",
"atlascloud": "atlascloud_api_key",
"requesty": "requesty_api_key",
"deepseek": "deepseek_api_key",
"zhipu": "zhipu_api_key",
+4
View File
@@ -166,6 +166,7 @@ class EvoScientistConfig:
minimax_base_url: str = ""
siliconflow_api_key: str = ""
openrouter_api_key: str = ""
atlascloud_api_key: str = ""
requesty_api_key: str = ""
deepseek_api_key: str = ""
zhipu_api_key: str = ""
@@ -761,6 +762,7 @@ _ENV_MAPPINGS = {
"minimax_base_url": "MINIMAX_BASE_URL",
"siliconflow_api_key": "SILICONFLOW_API_KEY",
"openrouter_api_key": "OPENROUTER_API_KEY",
"atlascloud_api_key": "ATLASCLOUD_API_KEY",
"requesty_api_key": "REQUESTY_API_KEY",
"deepseek_api_key": "DEEPSEEK_API_KEY",
"zhipu_api_key": "ZHIPU_API_KEY",
@@ -939,6 +941,8 @@ def apply_config_to_env(config: EvoScientistConfig) -> None:
os.environ["SILICONFLOW_API_KEY"] = config.siliconflow_api_key
if config.openrouter_api_key and not os.environ.get("OPENROUTER_API_KEY"):
os.environ["OPENROUTER_API_KEY"] = config.openrouter_api_key
if config.atlascloud_api_key and not os.environ.get("ATLASCLOUD_API_KEY"):
os.environ["ATLASCLOUD_API_KEY"] = config.atlascloud_api_key
if config.requesty_api_key and not os.environ.get("REQUESTY_API_KEY"):
os.environ["REQUESTY_API_KEY"] = config.requesty_api_key
if config.deepseek_api_key and not os.environ.get("DEEPSEEK_API_KEY"):
+1
View File
@@ -176,6 +176,7 @@ def _redact_api_keys(message: str) -> str:
_HOST_TO_PROVIDER: dict[str, str] = {
"api.openai.com": "openai",
"api.anthropic.com": "anthropic",
"api.atlascloud.ai": "atlascloud",
"api.deepseek.com": "deepseek",
"api.moonshot.cn": "moonshot",
"api.siliconflow.cn": "siliconflow",
+7 -3
View File
@@ -1,9 +1,9 @@
"""LLM model configuration based on LangChain init_chat_model.
This module provides a unified interface for creating chat model instances
with support for multiple providers (Anthropic, OpenAI, Google GenAI, MiniMax
(Anthropic-compatible), NVIDIA, SiliconFlow, OpenRouter, Requesty, ZhipuAI,
Volcengine, DashScope, DashScope-Code, DeepSeek, Ollama, and custom
with support for multiple providers (Anthropic, OpenAI, Google GenAI, Atlas
Cloud, MiniMax (Anthropic-compatible), NVIDIA, SiliconFlow, OpenRouter, Requesty,
ZhipuAI, Volcengine, DashScope, DashScope-Code, DeepSeek, Ollama, and custom
OpenAI/Anthropic-compatible endpoints) and convenient short names for common
models.
"""
@@ -46,6 +46,7 @@ _VOLCENGINE_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
_DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
_DASHSCOPE_CODE_BASE_URL = "https://coding.dashscope.aliyuncs.com/v1"
_ATLASCLOUD_BASE_URL = "https://api.atlascloud.ai/v1"
_MOONSHOT_BASE_URL = "https://api.moonshot.cn/v1"
_KIMI_CODING_BASE_URL = "https://api.kimi.com/coding/"
_REQUESTY_BASE_URL = "https://router.requesty.ai/v1"
@@ -107,6 +108,7 @@ def _is_deepseek_endpoint(base_url: str | None) -> bool:
# Providers routed through the OpenAI provider with a custom base_url.
# Maps provider name → (base_url or None, env var for API key).
_OPENAI_ROUTED_PROVIDERS: dict[str, tuple[str | None, str]] = {
"atlascloud": (_ATLASCLOUD_BASE_URL, "ATLASCLOUD_API_KEY"),
"moonshot": (_MOONSHOT_BASE_URL, "MOONSHOT_API_KEY"),
"siliconflow": (_SILICONFLOW_BASE_URL, "SILICONFLOW_API_KEY"),
"zhipu": (_ZHIPU_BASE_URL, "ZHIPU_API_KEY"),
@@ -171,6 +173,8 @@ _MODEL_ENTRIES: list[tuple[str, str, str]] = [
("gpt-5.4", "gpt-5.4", "custom-openai"),
("gpt-5.3-codex", "gpt-5.3-codex", "custom-openai"),
("gpt-5-mini", "gpt-5-mini", "custom-openai"),
# Atlas Cloud (OpenAI-compatible)
("qwen3.5-27b", "qwen/qwen3.5-27b", "atlascloud"),
# Anthropic (current generation)
("claude-fable-5", "claude-fable-5", "anthropic"),
("claude-opus-5", "claude-opus-5", "anthropic"),
+5
View File
@@ -57,6 +57,7 @@ def temp_config_dir(tmp_path, monkeypatch):
for key in [
"ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"ATLASCLOUD_API_KEY",
"TAVILY_API_KEY",
"EVOSCIENTIST_DEFAULT_MODE",
"EVOSCIENTIST_WORKSPACE_DIR",
@@ -87,6 +88,7 @@ def clean_env(monkeypatch):
for key in [
"ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"ATLASCLOUD_API_KEY",
"TAVILY_API_KEY",
"EVOSCIENTIST_DEFAULT_MODE",
"EVOSCIENTIST_WORKSPACE_DIR",
@@ -713,6 +715,7 @@ class TestApplyConfigToEnv:
config = EvoScientistConfig(
anthropic_api_key="config-ant-key",
openai_api_key="config-oai-key",
atlascloud_api_key="config-atlas-key",
tavily_api_key="config-tav-key",
)
@@ -720,6 +723,7 @@ class TestApplyConfigToEnv:
assert os.environ.get("ANTHROPIC_API_KEY") == "config-ant-key"
assert os.environ.get("OPENAI_API_KEY") == "config-oai-key"
assert os.environ.get("ATLASCLOUD_API_KEY") == "config-atlas-key"
assert os.environ.get("TAVILY_API_KEY") == "config-tav-key"
def test_does_not_override_existing_env(self, monkeypatch):
@@ -738,6 +742,7 @@ class TestApplyConfigToEnv:
assert os.environ.get("ANTHROPIC_API_KEY") is None
assert os.environ.get("OPENAI_API_KEY") is None
assert os.environ.get("ATLASCLOUD_API_KEY") is None
assert os.environ.get("EVOSCIENTIST_OPENROUTER_ANTHROPIC_PROMPT_CACHE") is None
def test_openrouter_anthropic_prompt_cache_opt_out_applied(
+29
View File
@@ -46,6 +46,7 @@ class TestModelsRegistry:
assert "deepseek" in providers
assert "moonshot" in providers
assert "kimi-coding" in providers
assert "atlascloud" in providers
def test_entries_are_valid_tuples(self):
"""Test that _MODEL_ENTRIES contains valid (name, model_id, provider) tuples."""
@@ -68,6 +69,7 @@ class TestModelsRegistry:
"deepseek",
"moonshot",
"kimi-coding",
"atlascloud",
}
for entry in _MODEL_ENTRIES:
assert len(entry) == 3, f"Entry {entry} doesn't have 3 elements"
@@ -91,6 +93,9 @@ class TestModelsRegistry:
assert len(openrouter_models) > 0
siliconflow_models = get_models_for_provider("siliconflow")
assert len(siliconflow_models) > 0
atlas_models = get_models_for_provider("atlascloud")
assert ("qwen3.5-27b", "qwen/qwen3.5-27b") in atlas_models
assert get_models_for_provider("atlas") == []
# =============================================================================
@@ -390,6 +395,30 @@ def _sdk_retry_supports_status_codes_override() -> bool:
class TestThirdPartyRouting:
@patch("EvoScientist.llm.models.init_chat_model")
def test_atlascloud_routes_through_openai(self, mock_init, monkeypatch):
"""Atlas Cloud should use OpenAI-compatible routing with its default URL."""
mock_init.return_value = "mock_model"
monkeypatch.setenv("ATLASCLOUD_API_KEY", "atlas-key-123")
get_chat_model("qwen3.5-27b", provider="atlascloud")
call_kwargs = mock_init.call_args[1]
assert call_kwargs["model"] == "qwen/qwen3.5-27b"
assert call_kwargs["model_provider"] == "openai"
assert call_kwargs["base_url"] == "https://api.atlascloud.ai/v1"
assert call_kwargs["api_key"] == "atlas-key-123"
assert "reasoning" not in call_kwargs
def test_atlascloud_host_maps_to_provider(self):
"""Provider error envelopes should identify Atlas Cloud by host."""
from EvoScientist.llm.errors import _lookup_host_or_compat
assert (
_lookup_host_or_compat("https://api.atlascloud.ai/v1", "openai")
== "atlascloud"
)
@patch("EvoScientist.llm.models.init_chat_model")
def test_siliconflow_routes_through_openai(self, mock_init, monkeypatch):
"""SiliconFlow provider should route through OpenAI with correct base_url."""
+56
View File
@@ -373,6 +373,62 @@ class TestChoiceValidator:
assert "one of" in str(exc_info.value.message)
class TestValidateAtlasCloudKey:
def test_empty_key_skipped(self):
from EvoScientist.config.onboard.validators import validate_atlascloud_key
is_valid, msg = validate_atlascloud_key("")
assert is_valid is True
assert "Skipped" in msg
@pytest.mark.parametrize("status", [200, 404])
def test_accepts_authenticated_sentinel_statuses(self, status):
from EvoScientist.config.onboard.validators import validate_atlascloud_key
with patch("httpx.post") as mock_post:
mock_post.return_value.status_code = status
is_valid, msg = validate_atlascloud_key("atlas-key")
assert is_valid is True
assert msg == "Valid"
payload = mock_post.call_args.kwargs["json"]
assert payload["model"] == "atlascloud/auth-preflight"
assert payload["max_tokens"] == 1
@pytest.mark.parametrize("status", [401, 403])
def test_auth_rejection_is_invalid(self, status):
from EvoScientist.config.onboard.validators import validate_atlascloud_key
with patch("httpx.post") as mock_post:
mock_post.return_value.status_code = status
is_valid, msg = validate_atlascloud_key("bad-key")
assert is_valid is False
assert msg == "Invalid API key"
def test_insufficient_balance_means_valid_key(self):
"""402 fires before model resolution, so auth passed — key is valid."""
from EvoScientist.config.onboard.validators import validate_atlascloud_key
with patch("httpx.post") as mock_post:
mock_post.return_value.status_code = 402
is_valid, msg = validate_atlascloud_key("atlas-key")
assert is_valid is True
assert "insufficient balance" in msg.lower()
@pytest.mark.parametrize("status", [400, 429, 500, 503])
def test_transient_status_is_inconclusive(self, status):
from EvoScientist.config.onboard.validators import validate_atlascloud_key
with patch("httpx.post") as mock_post:
mock_post.return_value.status_code = status
is_valid, msg = validate_atlascloud_key("atlas-key")
assert is_valid is False
assert "inconclusive" in msg.lower()
# =============================================================================
# Test Step Functions (Mocked questionary)
# =============================================================================