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
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
# ANTHROPIC_AUTH_TOKEN=
# ANTHROPIC_BASE_URL=

# Atlas Cloud(OpenAI 兼容);启用时设置 LLM_PROVIDER=atlascloud
# LLM_PROVIDER=atlascloud
# ATLASCLOUD_API_KEY=
# ATLASCLOUD_BASE_URL=https://api.atlascloud.ai/v1
# ATLASCLOUD_MODEL=deepseek-ai/deepseek-v4-pro

# 与 aivideo 共用方舟凭证
ARK_API_KEY=
ARK_BASE_URL=https://ark.cn-beijing.volces.com/api/v3/chat/completions
Expand Down
37 changes: 35 additions & 2 deletions application/ai/llm_control_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
from pydantic import BaseModel, Field, ConfigDict, field_validator, model_validator

from domain.ai.services.llm_service import DEFAULT_MAX_OUTPUT_TOKENS
from infrastructure.ai.llm_environment import LLMEnvironmentSettings
from infrastructure.ai.llm_environment import (
ATLAS_CLOUD_DEFAULT_BASE_URL,
ATLAS_CLOUD_DEFAULT_MODEL,
LLMEnvironmentSettings,
)
from infrastructure.persistence.database.connection import get_database
from infrastructure.ai.url_utils import (
normalize_anthropic_base_url,
Expand Down Expand Up @@ -207,6 +211,15 @@ def get_presets(self) -> List[LLMPreset]:
description='OpenAI 官方接口(自动兼容底层 Responses API 与 Chat Completions)。导入后请在「模型名」填写所用模型 ID。',
tags=['official'],
),
LLMPreset(
key='atlascloud',
label='Atlas Cloud',
protocol='openai',
default_base_url=ATLAS_CLOUD_DEFAULT_BASE_URL,
default_model=ATLAS_CLOUD_DEFAULT_MODEL,
description='Atlas Cloud OpenAI-compatible 接口, 支持统一访问多种文本模型。',
tags=['cloud', 'openai-compatible', 'preset'],
),
LLMPreset(
key='claude-official',
label='Claude / Anthropic 官方',
Expand Down Expand Up @@ -388,12 +401,16 @@ def resolve_profile(self, profile: LLMProfile) -> LLMProfile:
profile.base_url.strip() or (preset.default_base_url if preset else ''),
)
model = profile.model.strip() or (preset.default_model if preset else '')
use_legacy_chat_completions = (
profile.use_legacy_chat_completions or profile.preset_key == 'atlascloud'
)
return LLMProfile(
**{
**profile.model_dump(),
'protocol': protocol,
'base_url': base_url,
'model': model,
'use_legacy_chat_completions': use_legacy_chat_completions,
}
)

Expand Down Expand Up @@ -574,8 +591,24 @@ def _build_initial_config(self) -> LLMControlConfig:
openai_key = env.openai_api_key
gemini_key = env.gemini_api_key
ark_key = env.ark_api_key
atlascloud_key = env.atlascloud_api_key

if anthropic_key and (env.provider == 'anthropic' or not env.provider):
if atlascloud_key and (
env.provider == 'atlascloud'
or (
not env.provider
and not any((anthropic_key, openai_key, gemini_key, ark_key))
)
):
profiles[0] = profiles[0].model_copy(update={
'name': 'Atlas Cloud',
'preset_key': 'atlascloud',
'api_key': atlascloud_key,
'base_url': env.atlascloud_base_url_or_default,
'model': env.atlascloud_model_or_default,
})
active_profile_id = profiles[0].id
elif anthropic_key and (env.provider == 'anthropic' or not env.provider):
profiles[1] = profiles[1].model_copy(update={
'api_key': anthropic_key,
'base_url': env.anthropic_base_url or profiles[1].base_url,
Expand Down
16 changes: 16 additions & 0 deletions infrastructure/ai/llm_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@


ARK_DEFAULT_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
ATLAS_CLOUD_DEFAULT_BASE_URL = "https://api.atlascloud.ai/v1"
ATLAS_CLOUD_DEFAULT_MODEL = "deepseek-ai/deepseek-v4-pro"


def _env_text(name: str, default: str = "") -> str:
Expand All @@ -29,6 +31,9 @@ class LLMEnvironmentSettings:
gemini_api_key: str = ""
gemini_base_url: str = ""
gemini_model: str = ""
atlascloud_api_key: str = ""
atlascloud_base_url: str = ""
atlascloud_model: str = ""
ark_api_key: str = ""
ark_base_url: str = ""
ark_model: str = ""
Expand All @@ -49,6 +54,9 @@ def from_env(cls) -> "LLMEnvironmentSettings":
gemini_api_key=_env_text("GEMINI_API_KEY"),
gemini_base_url=_env_text("GEMINI_BASE_URL"),
gemini_model=_env_text("GEMINI_MODEL"),
atlascloud_api_key=_env_text("ATLASCLOUD_API_KEY"),
atlascloud_base_url=_env_text("ATLASCLOUD_BASE_URL"),
atlascloud_model=_env_text("ATLASCLOUD_MODEL"),
ark_api_key=_env_text("ARK_API_KEY"),
ark_base_url=_env_text("ARK_BASE_URL"),
ark_model=_env_text("ARK_MODEL"),
Expand All @@ -64,6 +72,14 @@ def openai_preset_key(self) -> str:
return "custom-openai-compatible"
return "openai-official"

@property
def atlascloud_base_url_or_default(self) -> str:
return self.atlascloud_base_url or ATLAS_CLOUD_DEFAULT_BASE_URL

@property
def atlascloud_model_or_default(self) -> str:
return self.atlascloud_model or ATLAS_CLOUD_DEFAULT_MODEL

@property
def ark_base_url_or_default(self) -> str:
return self.ark_base_url or ARK_DEFAULT_BASE_URL
53 changes: 52 additions & 1 deletion tests/unit/application/ai/test_llm_control_service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from application.ai.llm_control_service import LLMControlService, LLMProfile
from domain.ai.services.llm_service import DEFAULT_MAX_OUTPUT_TOKENS
from infrastructure.ai.llm_environment import ARK_DEFAULT_BASE_URL
from infrastructure.ai.llm_environment import (
ARK_DEFAULT_BASE_URL,
ATLAS_CLOUD_DEFAULT_BASE_URL,
ATLAS_CLOUD_DEFAULT_MODEL,
)


LLM_ENV_NAMES = (
Expand All @@ -15,6 +19,9 @@
"GEMINI_API_KEY",
"GEMINI_BASE_URL",
"GEMINI_MODEL",
"ATLASCLOUD_API_KEY",
"ATLASCLOUD_BASE_URL",
"ATLASCLOUD_MODEL",
"ARK_API_KEY",
"ARK_BASE_URL",
"ARK_MODEL",
Expand Down Expand Up @@ -79,6 +86,50 @@ def test_initial_config_keeps_ark_default_base_url(monkeypatch):
assert active.model == "ark-model"


def test_atlascloud_preset_has_working_defaults():
preset = LLMControlService().get_preset_map()["atlascloud"]

assert preset.protocol == "openai"
assert preset.default_base_url == ATLAS_CLOUD_DEFAULT_BASE_URL
assert preset.default_model == ATLAS_CLOUD_DEFAULT_MODEL


def test_initial_config_uses_atlascloud_environment(monkeypatch):
_clear_llm_env(monkeypatch)
monkeypatch.setenv("LLM_PROVIDER", "atlascloud")
monkeypatch.setenv("ATLASCLOUD_API_KEY", "atlas-key")

config = LLMControlService()._build_initial_config()

active = config.profiles[0]
assert config.active_profile_id == active.id
assert active.name == "Atlas Cloud"
assert active.preset_key == "atlascloud"
assert active.base_url == ATLAS_CLOUD_DEFAULT_BASE_URL
assert active.model == ATLAS_CLOUD_DEFAULT_MODEL


def test_atlascloud_key_does_not_change_existing_default_priority(monkeypatch):
_clear_llm_env(monkeypatch)
monkeypatch.setenv("ATLASCLOUD_API_KEY", "atlas-key")
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", "anthropic-token")

config = LLMControlService()._build_initial_config()

assert config.active_profile_id == "claude-official-default"
assert config.profiles[1].api_key == "anthropic-token"


def test_resolve_atlascloud_profile_uses_chat_completions():
resolved = LLMControlService().resolve_profile(
LLMProfile(id="atlas", name="Atlas Cloud", preset_key="atlascloud")
)

assert resolved.base_url == ATLAS_CLOUD_DEFAULT_BASE_URL
assert resolved.model == ATLAS_CLOUD_DEFAULT_MODEL
assert resolved.use_legacy_chat_completions is True


def test_profile_lifts_small_max_tokens_to_global_floor():
profile = LLMProfile(id="p", name="Profile", max_tokens=4096)

Expand Down
20 changes: 20 additions & 0 deletions tests/unit/infrastructure/ai/test_llm_environment.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from infrastructure.ai.llm_environment import (
ARK_DEFAULT_BASE_URL,
ATLAS_CLOUD_DEFAULT_BASE_URL,
ATLAS_CLOUD_DEFAULT_MODEL,
LLMEnvironmentSettings,
)

Expand All @@ -18,6 +20,9 @@
"GEMINI_API_KEY",
"GEMINI_BASE_URL",
"GEMINI_MODEL",
"ATLASCLOUD_API_KEY",
"ATLASCLOUD_BASE_URL",
"ATLASCLOUD_MODEL",
"ARK_API_KEY",
"ARK_BASE_URL",
"ARK_MODEL",
Expand All @@ -37,6 +42,8 @@ def test_llm_environment_defaults(monkeypatch):
assert settings.provider == ""
assert settings.anthropic_api_key_with_token_fallback == ""
assert settings.openai_preset_key == "openai-official"
assert settings.atlascloud_base_url_or_default == ATLAS_CLOUD_DEFAULT_BASE_URL
assert settings.atlascloud_model_or_default == ATLAS_CLOUD_DEFAULT_MODEL
assert settings.ark_base_url_or_default == ARK_DEFAULT_BASE_URL


Expand Down Expand Up @@ -88,3 +95,16 @@ def test_llm_environment_ark_base_url_default_and_override(monkeypatch):

monkeypatch.setenv("ARK_BASE_URL", "https://ark.example/api/v3")
assert LLMEnvironmentSettings.from_env().ark_base_url_or_default == "https://ark.example/api/v3"


def test_llm_environment_atlascloud_overrides(monkeypatch):
_clear_llm_env(monkeypatch)
monkeypatch.setenv("ATLASCLOUD_API_KEY", "atlas-key")
monkeypatch.setenv("ATLASCLOUD_BASE_URL", "https://atlas.example/v1")
monkeypatch.setenv("ATLASCLOUD_MODEL", "vendor/model")

settings = LLMEnvironmentSettings.from_env()

assert settings.atlascloud_api_key == "atlas-key"
assert settings.atlascloud_base_url_or_default == "https://atlas.example/v1"
assert settings.atlascloud_model_or_default == "vendor/model"