Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -1352,6 +1352,19 @@
"proxy": "",
"custom_headers": {},
},
"Atlas Cloud": {
"id": "atlascloud",
"provider": "atlascloud",
"type": "atlascloud_chat_completion",
"provider_type": "chat_completion",
"enable": True,
"key": [],
"timeout": 120,
"api_base": "https://api.atlascloud.ai/v1",
"model": "qwen/qwen3.5-flash",
"proxy": "",
"custom_headers": {},
},
"NVIDIA": {
"id": "nvidia",
"provider": "nvidia",
Expand Down
4 changes: 4 additions & 0 deletions astrbot/core/provider/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,10 @@ def dynamic_import_provider(self, type: str) -> None:
from .sources.openrouter_source import (
ProviderOpenRouter as ProviderOpenRouter,
)
case "atlascloud_chat_completion":
from .sources.atlascloud_source import (
ProviderAtlasCloud as ProviderAtlasCloud,
)
case "anthropic_chat_completion":
from .sources.anthropic_source import (
ProviderAnthropic as ProviderAnthropic,
Expand Down
40 changes: 40 additions & 0 deletions astrbot/core/provider/sources/atlascloud_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from ..register import register_provider_adapter
from .openai_source import ProviderOpenAIOfficial

ATLASCLOUD_DEFAULT_API_BASE = "https://api.atlascloud.ai/v1"
ATLASCLOUD_DEFAULT_MODEL = "qwen/qwen3.5-flash"
ATLASCLOUD_MODELS = [
"qwen/qwen3.5-flash",
Comment on lines +5 to +7

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Keep the default model DRY by reusing ATLASCLOUD_DEFAULT_MODEL in ATLASCLOUD_MODELS.

The default model string is defined both in ATLASCLOUD_DEFAULT_MODEL and in ATLASCLOUD_MODELS, so any change would require updating two places. Referencing the constant in the list (e.g., ATLASCLOUD_MODELS = [ATLASCLOUD_DEFAULT_MODEL, ...]) keeps them in sync.

"deepseek-ai/deepseek-v4-pro",
"deepseek-ai/deepseek-v4-flash",
]


@register_provider_adapter(
"atlascloud_chat_completion",
"Atlas Cloud Chat Completion Provider Adapter",
)
class ProviderAtlasCloud(ProviderOpenAIOfficial):
"""Atlas Cloud provider using its OpenAI-compatible LLM endpoint."""

def __init__(
self,
provider_config: dict,
provider_settings: dict,
) -> None:
if not provider_config.get("api_base"):
provider_config["api_base"] = ATLASCLOUD_DEFAULT_API_BASE
if not provider_config.get("model"):
provider_config["model"] = ATLASCLOUD_DEFAULT_MODEL

super().__init__(provider_config, provider_settings)

async def get_models(self) -> list[str]:
try:
models = await super().get_models()
if models:
return models
except Exception:
Comment on lines +32 to +37

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): The broad exception handler in get_models silently swallows all errors, which could make debugging difficult.

Catching Exception and then pass-ing will mask real issues (auth, network, schema changes, etc.). Please either narrow the exception type or at least log the failure with enough context so provider-related problems can be diagnosed while still falling back to the static list.

Suggested implementation:

    async def get_models(self) -> list[str]:
        try:
            models = await super().get_models()
            if models:
                return models
        except Exception as exc:
            logger.warning(
                "AtlasCloudSource.get_models: failed to fetch models from upstream; "
                "falling back to static ATLASCLOUD_MODELS list.",
                exc_info=exc,
            )

        return ATLASCLOUD_MODELS

To fully implement this change, ensure there is a module-level logger defined in this file, for example:

  1. At the top of astrbot/core/provider/sources/atlascloud_source.py:
    • Add import logging if it is not already present.
    • Add logger = logging.getLogger(__name__) alongside other module-level definitions.

If this project uses a different logging convention (e.g., a shared logger utility), replace logger = logging.getLogger(__name__) and the logger.warning(...) call with the appropriate project-specific logger.

pass

return ATLASCLOUD_MODELS.copy()
36 changes: 36 additions & 0 deletions tests/test_openai_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import astrbot.core.provider.sources.request_retry as request_retry
from astrbot.core.exceptions import EmptyModelOutputError
from astrbot.core.provider.entities import LLMResponse
from astrbot.core.provider.sources.atlascloud_source import ProviderAtlasCloud
from astrbot.core.provider.sources.groq_source import ProviderGroq
from astrbot.core.provider.sources.openai_source import ProviderOpenAIOfficial
from astrbot.core.utils.media_utils import ResolvedMediaData, file_uri_to_path
Expand Down Expand Up @@ -60,6 +61,22 @@ def _make_groq_provider(overrides: dict | None = None) -> ProviderGroq:
)


def _make_atlascloud_provider(
overrides: dict | None = None,
) -> ProviderAtlasCloud:
provider_config = {
"id": "test-atlascloud",
"type": "atlascloud_chat_completion",
"key": ["test-key"],
}
if overrides:
provider_config.update(overrides)
return ProviderAtlasCloud(
provider_config=provider_config,
provider_settings={},
)


def test_create_http_client_uses_openai_httpx_module(monkeypatch):
captured: dict[str, object] = {}

Expand Down Expand Up @@ -120,6 +137,25 @@ def fake_import(name, globals=None, locals=None, fromlist=(), level=0):
assert captured["httpx_module"] is openai_source_module.httpx


def test_atlascloud_provider_sets_default_openai_compatible_endpoint():
provider = _make_atlascloud_provider()

assert str(provider.client.base_url).rstrip("/") == "https://api.atlascloud.ai/v1"
assert provider.get_model() == "qwen/qwen3.5-flash"
Comment on lines +140 to +144

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question (testing): Consider adding tests for explicit-but-falsy api_base/model values (e.g. empty strings)

ProviderAtlasCloud treats if not provider_config.get("api_base") / if not provider_config.get("model") as “missing”, so values like "" or None are replaced with defaults. Current tests only cover absent or non-empty values.

Please add tests that exercise falsy-but-explicit values, for example:

  • {"api_base": "", "model": ""} to confirm that defaults are applied (if that’s the intended behavior), or
  • A case that asserts empty strings are preserved, if that’s what we want.

This will clarify and lock in the expected behavior for falsy config values.



def test_atlascloud_provider_keeps_custom_endpoint_and_model():
provider = _make_atlascloud_provider(
{
"api_base": "https://proxy.example.com/v1",
"model": "deepseek-ai/deepseek-v4-pro",
},
)

assert str(provider.client.base_url).rstrip("/") == "https://proxy.example.com/v1"
assert provider.get_model() == "deepseek-ai/deepseek-v4-pro"


@pytest.mark.asyncio
async def test_get_models_retries_transient_request_error(monkeypatch):
monkeypatch.setattr(request_retry, "REQUEST_RETRY_WAIT_MIN_S", 0)
Expand Down