mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-19 19:30:29 +08:00
feat: add Atlas Cloud as LLM/AI provider (#2279)
* feat: add Atlas Cloud as OpenAI-compatible LLM provider - Add Atlas Cloud env vars to .env.example (ATLAS_API_KEY, ATLAS_BASE_URL) - Add docs/ATLAS-CLOUD-GUIDE.md with configuration, model list, and usage example - Atlas Cloud provides 59+ LLM models via OpenAI-compatible API at https://api.atlascloud.ai/v1 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(atlascloud): add Atlas Cloud provider implementation Wire Atlas Cloud in as a first-class OpenAI-compatible LLM provider, complementing the existing .env.example/docs entries. - src/llm/providers/atlas.py: AtlasProvider adapter (base_url https://api.atlascloud.ai/v1, default model deepseek-ai/deepseek-v4-pro); floors max_tokens to 512 for reasoning models; reads ATLAS_API_KEY (falls back to ATLASCLOUD_API_KEY), ATLAS_BASE_URL, ATLAS_MODEL - src/llm/core/types.py: add ProviderType.ATLAS - providers __init__/resolver: export + register AtlasProvider - tests: test_atlas_provider.py + resolver coverage for "atlas" Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ceca28852e
commit
71792fda81
@ -30,6 +30,13 @@ ASTRAFLOW_CN_API_KEY=
|
|||||||
# ASTRAFLOW_CN_MODEL=gpt-4o-mini
|
# ASTRAFLOW_CN_MODEL=gpt-4o-mini
|
||||||
# ASTRAFLOW_CN_BASE_URL=https://api.modelverse.cn/v1
|
# ASTRAFLOW_CN_BASE_URL=https://api.modelverse.cn/v1
|
||||||
|
|
||||||
|
# --- Optional: Atlas Cloud (OpenAI-compatible, 59+ LLM models) ---------------
|
||||||
|
# Full-modal AI inference platform — LLM / image / video generation
|
||||||
|
# Docs: https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=everything-claude-code
|
||||||
|
ATLAS_API_KEY=
|
||||||
|
# ATLAS_BASE_URL=https://api.atlascloud.ai/v1
|
||||||
|
# ATLAS_MODEL=anthropic/claude-sonnet-4.6
|
||||||
|
|
||||||
# ─── ECC agent data (multi-harness isolation) ───────────────────────────────
|
# ─── ECC agent data (multi-harness isolation) ───────────────────────────────
|
||||||
# Memory hooks (sessions, learned skills, aliases, metrics). Default: ~/.claude
|
# Memory hooks (sessions, learned skills, aliases, metrics). Default: ~/.claude
|
||||||
# Use a separate root when running ECC in Cursor alongside Claude Code:
|
# Use a separate root when running ECC in Cursor alongside Claude Code:
|
||||||
|
|||||||
69
docs/ATLAS-CLOUD-GUIDE.md
Normal file
69
docs/ATLAS-CLOUD-GUIDE.md
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# Atlas Cloud — LLM Provider Guide
|
||||||
|
|
||||||
|
[Atlas Cloud](https://www.atlascloud.ai/?utm_source=github&utm_medium=link&utm_campaign=everything-claude-code) is a full-modal AI inference platform providing an OpenAI-compatible API for 59+ LLM models, image generation, and video generation.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Set the following environment variables to use Atlas Cloud as your LLM backend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ATLAS_API_KEY=<your-atlascloud-api-key>
|
||||||
|
ATLAS_BASE_URL=https://api.atlascloud.ai/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
Or copy from `.env.example`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# Then fill in ATLAS_API_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
ECC can install its managed surfaces into any OpenAI-compatible backend. To use Atlas Cloud with Claude Code (or any ECC-managed harness), set the base URL and API key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ATLAS_API_KEY=your-key-here
|
||||||
|
export ATLAS_BASE_URL=https://api.atlascloud.ai/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Models
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>All Atlas Cloud LLM models (59+)</summary>
|
||||||
|
|
||||||
|
- **Anthropic**: `anthropic/claude-haiku-4.5-20251001`, `anthropic/claude-opus-4.8`, `anthropic/claude-sonnet-4.6`
|
||||||
|
- **OpenAI**: `openai/gpt-5.4`, `openai/gpt-5.5`
|
||||||
|
- **Google Gemini**: `google/gemini-3.1-flash-lite`, `google/gemini-3.1-pro-preview`, `google/gemini-3.5-flash`
|
||||||
|
- **Qwen**: `qwen/qwen2.5-7b-instruct`, `Qwen/Qwen3-235B-A22B-Instruct-2507`, `qwen/qwen3-235b-a22b-thinking-2507`, `qwen/qwen3-30b-a3b`, `Qwen/Qwen3-30B-A3B-Instruct-2507`, `qwen/qwen3-30b-a3b-thinking-2507`, `qwen/qwen3-32b`, `qwen/qwen3-8b`, `Qwen/Qwen3-Coder`, `qwen/qwen3-coder-next`, `qwen/qwen3-max-2026-01-23`, `Qwen/Qwen3-Next-80B-A3B-Instruct`, `Qwen/Qwen3-Next-80B-A3B-Thinking`, `Qwen/Qwen3-VL-235B-A22B-Instruct`, `qwen/qwen3-vl-235b-a22b-thinking`, `qwen/qwen3-vl-30b-a3b-instruct`, `qwen/qwen3-vl-30b-a3b-thinking`, `qwen/qwen3-vl-8b-instruct`, `qwen/qwen3.5-122b-a10b`, `qwen/qwen3.5-27b`, `qwen/qwen3.5-35b-a3b`, `qwen/qwen3.5-397b-a17b`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-plus`
|
||||||
|
- **DeepSeek**: `deepseek-ai/deepseek-ocr`, `deepseek-ai/deepseek-r1-0528`, `deepseek-ai/DeepSeek-V3-0324`, `deepseek-ai/DeepSeek-V3.1`, `deepseek-ai/DeepSeek-V3.1-Terminus`, `deepseek-ai/deepseek-v3.2`, `deepseek-ai/DeepSeek-V3.2-Exp`, `deepseek-ai/deepseek-v4-flash`, `deepseek-ai/deepseek-v4-pro`
|
||||||
|
- **Kimi**: `moonshotai/Kimi-K2-Instruct`, `moonshotai/Kimi-K2-Instruct-0905`, `moonshotai/Kimi-K2-Thinking`, `moonshotai/kimi-k2.5`, `moonshotai/kimi-k2.6`
|
||||||
|
- **GLM**: `zai-org/GLM-4.6`, `zai-org/glm-4.7`, `zai-org/glm-5`, `zai-org/glm-5-turbo`, `zai-org/glm-5.1`, `zai-org/glm-5v-turbo`
|
||||||
|
- **MiniMax**: `MiniMaxAI/MiniMax-M2`, `minimaxai/minimax-m2.1`, `minimaxai/minimax-m2.5`, `minimaxai/minimax-m2.7`
|
||||||
|
- **xAI**: `xai/grok-4.3`
|
||||||
|
- **KAT**: `kwaipilot/kat-coder-pro-v2`
|
||||||
|
- **Other**: `owl`
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from openai import OpenAI
|
||||||
|
import os
|
||||||
|
|
||||||
|
client = OpenAI(
|
||||||
|
api_key=os.environ["ATLAS_API_KEY"],
|
||||||
|
base_url=os.environ.get("ATLAS_BASE_URL", "https://api.atlascloud.ai/v1"),
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="anthropic/claude-sonnet-4.6",
|
||||||
|
messages=[{"role": "user", "content": "Hello from ECC + Atlas Cloud!"}],
|
||||||
|
)
|
||||||
|
print(response.choices[0].message.content)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Get API Credits
|
||||||
|
|
||||||
|
Visit [Atlas Cloud Coding Plan](https://www.atlascloud.ai/console/coding-plan) for API credits.
|
||||||
@ -20,6 +20,7 @@ class ProviderType(str, Enum):
|
|||||||
OLLAMA = "ollama"
|
OLLAMA = "ollama"
|
||||||
ASTRAFLOW = "astraflow"
|
ASTRAFLOW = "astraflow"
|
||||||
ASTRAFLOW_CN = "astraflow_cn"
|
ASTRAFLOW_CN = "astraflow_cn"
|
||||||
|
ATLAS = "atlas"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"""Provider adapters for multiple LLM backends."""
|
"""Provider adapters for multiple LLM backends."""
|
||||||
|
|
||||||
from llm.providers.astraflow import AstraflowCNProvider, AstraflowProvider
|
from llm.providers.astraflow import AstraflowCNProvider, AstraflowProvider
|
||||||
|
from llm.providers.atlas import AtlasProvider
|
||||||
from llm.providers.claude import ClaudeProvider
|
from llm.providers.claude import ClaudeProvider
|
||||||
from llm.providers.openai import OpenAIProvider
|
from llm.providers.openai import OpenAIProvider
|
||||||
from llm.providers.ollama import OllamaProvider
|
from llm.providers.ollama import OllamaProvider
|
||||||
@ -9,6 +10,7 @@ from llm.providers.resolver import get_provider, register_provider
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
"AstraflowCNProvider",
|
"AstraflowCNProvider",
|
||||||
"AstraflowProvider",
|
"AstraflowProvider",
|
||||||
|
"AtlasProvider",
|
||||||
"ClaudeProvider",
|
"ClaudeProvider",
|
||||||
"OpenAIProvider",
|
"OpenAIProvider",
|
||||||
"OllamaProvider",
|
"OllamaProvider",
|
||||||
|
|||||||
147
src/llm/providers/atlas.py
Normal file
147
src/llm/providers/atlas.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
"""Atlas Cloud OpenAI-compatible provider adapter."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
from llm.core.interface import (
|
||||||
|
AuthenticationError,
|
||||||
|
ContextLengthError,
|
||||||
|
LLMProvider,
|
||||||
|
RateLimitError,
|
||||||
|
)
|
||||||
|
from llm.core.types import LLMInput, LLMOutput, ModelInfo, ProviderType, ToolCall
|
||||||
|
from llm.providers.constants import EMPTY_FILTERED_RESPONSE_ERROR
|
||||||
|
|
||||||
|
ATLAS_BASE_URL = "https://api.atlascloud.ai/v1"
|
||||||
|
DEFAULT_ATLAS_MODEL = "deepseek-ai/deepseek-v4-pro"
|
||||||
|
# Reasoning models need enough headroom for their thinking budget plus the answer.
|
||||||
|
DEFAULT_ATLAS_MAX_TOKENS = 512
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_tool_arguments(raw_arguments: str | None) -> dict[str, Any]:
|
||||||
|
if not raw_arguments:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
arguments = json.loads(raw_arguments)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {"raw": raw_arguments}
|
||||||
|
|
||||||
|
if isinstance(arguments, dict):
|
||||||
|
return arguments
|
||||||
|
return {"value": arguments}
|
||||||
|
|
||||||
|
|
||||||
|
class AtlasProvider(LLMProvider):
|
||||||
|
"""Atlas Cloud endpoint using OpenAI-compatible chat completions.
|
||||||
|
|
||||||
|
Atlas Cloud (https://atlascloud.ai) exposes 300+ hosted models behind a
|
||||||
|
single OpenAI-compatible API, so it reuses the same chat-completions flow as
|
||||||
|
the other OpenAI-compatible adapters in this package.
|
||||||
|
"""
|
||||||
|
|
||||||
|
provider_type = ProviderType.ATLAS
|
||||||
|
# ``.env.example`` documents ATLAS_API_KEY; ATLASCLOUD_API_KEY is the name used
|
||||||
|
# by the Atlas Cloud SDK/skill, so accept either for convenience.
|
||||||
|
api_key_env = "ATLAS_API_KEY"
|
||||||
|
fallback_api_key_env = "ATLASCLOUD_API_KEY"
|
||||||
|
base_url_env = "ATLAS_BASE_URL"
|
||||||
|
model_env = "ATLAS_MODEL"
|
||||||
|
default_base_url = ATLAS_BASE_URL
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
api_key: str | None = None,
|
||||||
|
base_url: str | None = None,
|
||||||
|
default_model: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
self.api_key = (
|
||||||
|
api_key
|
||||||
|
or os.environ.get(self.api_key_env)
|
||||||
|
or os.environ.get(self.fallback_api_key_env)
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
self.base_url = base_url or os.environ.get(self.base_url_env, self.default_base_url)
|
||||||
|
env_model = os.environ.get(self.model_env)
|
||||||
|
self.default_model = default_model or env_model or DEFAULT_ATLAS_MODEL
|
||||||
|
self.client = OpenAI(api_key=self.api_key, base_url=self.base_url, _enforce_credentials=False)
|
||||||
|
self._models = [
|
||||||
|
ModelInfo(
|
||||||
|
name=self.default_model,
|
||||||
|
provider=self.provider_type,
|
||||||
|
supports_tools=True,
|
||||||
|
supports_vision=False,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def generate(self, llm_input: LLMInput) -> LLMOutput:
|
||||||
|
try:
|
||||||
|
params: dict[str, Any] = {
|
||||||
|
"model": llm_input.model or self.default_model,
|
||||||
|
"messages": [msg.to_dict() for msg in llm_input.messages],
|
||||||
|
}
|
||||||
|
if llm_input.temperature != 1.0:
|
||||||
|
params["temperature"] = llm_input.temperature
|
||||||
|
# Atlas reasoning models spend tokens on a thinking budget before the
|
||||||
|
# answer, so floor max_tokens to avoid truncated/empty completions.
|
||||||
|
max_tokens = llm_input.max_tokens
|
||||||
|
if max_tokens is None or max_tokens < DEFAULT_ATLAS_MAX_TOKENS:
|
||||||
|
max_tokens = DEFAULT_ATLAS_MAX_TOKENS
|
||||||
|
params["max_tokens"] = max_tokens
|
||||||
|
if llm_input.tools:
|
||||||
|
params["tools"] = [tool.to_openai_tool() for tool in llm_input.tools]
|
||||||
|
|
||||||
|
response = self.client.chat.completions.create(**params)
|
||||||
|
if not response.choices or response.choices[0].message is None:
|
||||||
|
raise ValueError(EMPTY_FILTERED_RESPONSE_ERROR)
|
||||||
|
choice = response.choices[0]
|
||||||
|
|
||||||
|
tool_calls = None
|
||||||
|
if choice.message.tool_calls:
|
||||||
|
tool_calls = [
|
||||||
|
ToolCall(
|
||||||
|
id=tc.id or "",
|
||||||
|
name=tc.function.name,
|
||||||
|
arguments=_parse_tool_arguments(tc.function.arguments),
|
||||||
|
)
|
||||||
|
for tc in choice.message.tool_calls
|
||||||
|
]
|
||||||
|
|
||||||
|
usage = None
|
||||||
|
if response.usage:
|
||||||
|
usage = {
|
||||||
|
"prompt_tokens": response.usage.prompt_tokens,
|
||||||
|
"completion_tokens": response.usage.completion_tokens,
|
||||||
|
"total_tokens": response.usage.total_tokens,
|
||||||
|
}
|
||||||
|
|
||||||
|
return LLMOutput(
|
||||||
|
content=choice.message.content or "",
|
||||||
|
tool_calls=tool_calls,
|
||||||
|
model=response.model,
|
||||||
|
usage=usage,
|
||||||
|
stop_reason=choice.finish_reason,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
msg = str(e)
|
||||||
|
if "401" in msg or "authentication" in msg.lower():
|
||||||
|
raise AuthenticationError(msg, provider=self.provider_type) from e
|
||||||
|
if "429" in msg or "rate_limit" in msg.lower():
|
||||||
|
raise RateLimitError(msg, provider=self.provider_type) from e
|
||||||
|
if "context" in msg.lower() and "length" in msg.lower():
|
||||||
|
raise ContextLengthError(msg, provider=self.provider_type) from e
|
||||||
|
raise
|
||||||
|
|
||||||
|
def list_models(self) -> list[ModelInfo]:
|
||||||
|
return self._models.copy()
|
||||||
|
|
||||||
|
def validate_config(self) -> bool:
|
||||||
|
return bool(self.api_key)
|
||||||
|
|
||||||
|
def get_default_model(self) -> str:
|
||||||
|
return self.default_model
|
||||||
@ -8,6 +8,7 @@ from pathlib import Path
|
|||||||
from llm.core.interface import LLMProvider
|
from llm.core.interface import LLMProvider
|
||||||
from llm.core.types import ProviderType
|
from llm.core.types import ProviderType
|
||||||
from llm.providers.astraflow import AstraflowCNProvider, AstraflowProvider
|
from llm.providers.astraflow import AstraflowCNProvider, AstraflowProvider
|
||||||
|
from llm.providers.atlas import AtlasProvider
|
||||||
from llm.providers.claude import ClaudeProvider
|
from llm.providers.claude import ClaudeProvider
|
||||||
from llm.providers.openai import OpenAIProvider
|
from llm.providers.openai import OpenAIProvider
|
||||||
from llm.providers.ollama import OllamaProvider
|
from llm.providers.ollama import OllamaProvider
|
||||||
@ -16,6 +17,7 @@ from llm.providers.ollama import OllamaProvider
|
|||||||
_PROVIDER_MAP: dict[ProviderType, type[LLMProvider]] = {
|
_PROVIDER_MAP: dict[ProviderType, type[LLMProvider]] = {
|
||||||
ProviderType.ASTRAFLOW: AstraflowProvider,
|
ProviderType.ASTRAFLOW: AstraflowProvider,
|
||||||
ProviderType.ASTRAFLOW_CN: AstraflowCNProvider,
|
ProviderType.ASTRAFLOW_CN: AstraflowCNProvider,
|
||||||
|
ProviderType.ATLAS: AtlasProvider,
|
||||||
ProviderType.CLAUDE: ClaudeProvider,
|
ProviderType.CLAUDE: ClaudeProvider,
|
||||||
ProviderType.OPENAI: OpenAIProvider,
|
ProviderType.OPENAI: OpenAIProvider,
|
||||||
ProviderType.OLLAMA: OllamaProvider,
|
ProviderType.OLLAMA: OllamaProvider,
|
||||||
|
|||||||
161
tests/test_atlas_provider.py
Normal file
161
tests/test_atlas_provider.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from llm.core.types import LLMInput, Message, ProviderType, Role, ToolCall, ToolDefinition
|
||||||
|
from llm.providers.atlas import ATLAS_BASE_URL, DEFAULT_ATLAS_MAX_TOKENS, DEFAULT_ATLAS_MODEL, AtlasProvider
|
||||||
|
|
||||||
|
|
||||||
|
def _tool() -> ToolDefinition:
|
||||||
|
return ToolDefinition(
|
||||||
|
name="search",
|
||||||
|
description="Search",
|
||||||
|
parameters={"type": "object", "properties": {"query": {"type": "string"}}},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _Completions:
|
||||||
|
def __init__(self, response: SimpleNamespace) -> None:
|
||||||
|
self.params = None
|
||||||
|
self.response = response
|
||||||
|
|
||||||
|
def create(self, **params):
|
||||||
|
self.params = params
|
||||||
|
return self.response
|
||||||
|
|
||||||
|
|
||||||
|
class _Client:
|
||||||
|
def __init__(self, response: SimpleNamespace) -> None:
|
||||||
|
self.completions = _Completions(response)
|
||||||
|
self.chat = SimpleNamespace(completions=self.completions)
|
||||||
|
|
||||||
|
|
||||||
|
def _response(**overrides) -> SimpleNamespace:
|
||||||
|
message = SimpleNamespace(content="ok", tool_calls=None)
|
||||||
|
choice = SimpleNamespace(message=message, finish_reason="stop")
|
||||||
|
defaults = {
|
||||||
|
"choices": [choice],
|
||||||
|
"model": DEFAULT_ATLAS_MODEL,
|
||||||
|
"usage": SimpleNamespace(prompt_tokens=1, completion_tokens=2, total_tokens=3),
|
||||||
|
}
|
||||||
|
defaults.update(overrides)
|
||||||
|
return SimpleNamespace(**defaults)
|
||||||
|
|
||||||
|
|
||||||
|
def test_atlas_provider_defaults_to_atlas_cloud_endpoint(monkeypatch):
|
||||||
|
monkeypatch.delenv("ATLAS_API_KEY", raising=False)
|
||||||
|
monkeypatch.delenv("ATLASCLOUD_API_KEY", raising=False)
|
||||||
|
monkeypatch.delenv("ATLAS_BASE_URL", raising=False)
|
||||||
|
monkeypatch.delenv("ATLAS_MODEL", raising=False)
|
||||||
|
|
||||||
|
provider = AtlasProvider()
|
||||||
|
|
||||||
|
assert provider.provider_type == ProviderType.ATLAS
|
||||||
|
assert provider.base_url == ATLAS_BASE_URL
|
||||||
|
assert provider.get_default_model() == DEFAULT_ATLAS_MODEL
|
||||||
|
assert provider.validate_config() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_atlas_provider_reads_env_overrides(monkeypatch):
|
||||||
|
monkeypatch.setenv("ATLAS_API_KEY", "atlas-key")
|
||||||
|
monkeypatch.setenv("ATLAS_MODEL", "deepseek-ai/deepseek-v3.2")
|
||||||
|
|
||||||
|
provider = AtlasProvider()
|
||||||
|
|
||||||
|
assert provider.get_default_model() == "deepseek-ai/deepseek-v3.2"
|
||||||
|
assert provider.validate_config() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_atlas_provider_accepts_atlascloud_api_key_fallback(monkeypatch):
|
||||||
|
monkeypatch.delenv("ATLAS_API_KEY", raising=False)
|
||||||
|
monkeypatch.setenv("ATLASCLOUD_API_KEY", "atlascloud-key")
|
||||||
|
|
||||||
|
provider = AtlasProvider()
|
||||||
|
|
||||||
|
assert provider.api_key == "atlascloud-key"
|
||||||
|
assert provider.validate_config() is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_atlas_provider_generates_openai_compatible_chat_completion():
|
||||||
|
provider = AtlasProvider(api_key="test", default_model=DEFAULT_ATLAS_MODEL)
|
||||||
|
client = _Client(_response(model=DEFAULT_ATLAS_MODEL))
|
||||||
|
provider.client = client
|
||||||
|
|
||||||
|
output = provider.generate(
|
||||||
|
LLMInput(
|
||||||
|
messages=[Message(role=Role.USER, content="hi")],
|
||||||
|
max_tokens=1024,
|
||||||
|
tools=[_tool()],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert output.content == "ok"
|
||||||
|
assert output.model == DEFAULT_ATLAS_MODEL
|
||||||
|
assert output.usage == {"prompt_tokens": 1, "completion_tokens": 2, "total_tokens": 3}
|
||||||
|
assert client.completions.params["model"] == DEFAULT_ATLAS_MODEL
|
||||||
|
assert client.completions.params["max_tokens"] == 1024
|
||||||
|
assert "temperature" not in client.completions.params
|
||||||
|
assert client.completions.params["tools"] == [
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "search",
|
||||||
|
"description": "Search",
|
||||||
|
"parameters": {"type": "object", "properties": {"query": {"type": "string"}}},
|
||||||
|
"strict": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_atlas_provider_floors_max_tokens_for_reasoning_models():
|
||||||
|
provider = AtlasProvider(api_key="test")
|
||||||
|
client = _Client(_response())
|
||||||
|
provider.client = client
|
||||||
|
|
||||||
|
# No max_tokens supplied -> floored to the reasoning default.
|
||||||
|
provider.generate(LLMInput(messages=[Message(role=Role.USER, content="hi")]))
|
||||||
|
assert client.completions.params["max_tokens"] == DEFAULT_ATLAS_MAX_TOKENS
|
||||||
|
|
||||||
|
# Too-small max_tokens is also raised to the floor.
|
||||||
|
provider.generate(LLMInput(messages=[Message(role=Role.USER, content="hi")], max_tokens=16))
|
||||||
|
assert client.completions.params["max_tokens"] == DEFAULT_ATLAS_MAX_TOKENS
|
||||||
|
|
||||||
|
|
||||||
|
def test_atlas_provider_forwards_non_default_temperature():
|
||||||
|
provider = AtlasProvider(api_key="test")
|
||||||
|
client = _Client(_response())
|
||||||
|
provider.client = client
|
||||||
|
|
||||||
|
provider.generate(LLMInput(messages=[Message(role=Role.USER, content="hi")], temperature=0.2))
|
||||||
|
|
||||||
|
assert client.completions.params["temperature"] == 0.2
|
||||||
|
|
||||||
|
|
||||||
|
def test_atlas_provider_parses_tool_calls():
|
||||||
|
provider = AtlasProvider(api_key="test")
|
||||||
|
tool_call = SimpleNamespace(
|
||||||
|
id="call_1",
|
||||||
|
function=SimpleNamespace(name="search", arguments='{"query":"atlas"}'),
|
||||||
|
)
|
||||||
|
message = SimpleNamespace(content="", tool_calls=[tool_call])
|
||||||
|
client = _Client(_response(choices=[SimpleNamespace(message=message, finish_reason="tool_calls")], usage=None))
|
||||||
|
provider.client = client
|
||||||
|
|
||||||
|
output = provider.generate(LLMInput(messages=[Message(role=Role.USER, content="hi")]))
|
||||||
|
|
||||||
|
assert output.tool_calls == [ToolCall(id="call_1", name="search", arguments={"query": "atlas"})]
|
||||||
|
assert output.usage is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_atlas_provider_preserves_malformed_tool_arguments():
|
||||||
|
provider = AtlasProvider(api_key="test")
|
||||||
|
tool_call = SimpleNamespace(
|
||||||
|
id="call_1",
|
||||||
|
function=SimpleNamespace(name="search", arguments="{not-json"),
|
||||||
|
)
|
||||||
|
message = SimpleNamespace(content="", tool_calls=[tool_call])
|
||||||
|
client = _Client(_response(choices=[SimpleNamespace(message=message, finish_reason="tool_calls")]))
|
||||||
|
provider.client = client
|
||||||
|
|
||||||
|
output = provider.generate(LLMInput(messages=[Message(role=Role.USER, content="hi")]))
|
||||||
|
|
||||||
|
assert output.tool_calls == [ToolCall(id="call_1", name="search", arguments={"raw": "{not-json"})]
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from llm.core.types import ProviderType
|
from llm.core.types import ProviderType
|
||||||
from llm.providers import AstraflowCNProvider, AstraflowProvider, ClaudeProvider, OpenAIProvider, OllamaProvider, get_provider
|
from llm.providers import AstraflowCNProvider, AstraflowProvider, AtlasProvider, ClaudeProvider, OpenAIProvider, OllamaProvider, get_provider
|
||||||
|
|
||||||
|
|
||||||
class TestGetProvider:
|
class TestGetProvider:
|
||||||
@ -29,6 +29,11 @@ class TestGetProvider:
|
|||||||
assert isinstance(provider, AstraflowCNProvider)
|
assert isinstance(provider, AstraflowCNProvider)
|
||||||
assert provider.provider_type == ProviderType.ASTRAFLOW_CN
|
assert provider.provider_type == ProviderType.ASTRAFLOW_CN
|
||||||
|
|
||||||
|
def test_get_atlas_provider(self):
|
||||||
|
provider = get_provider("atlas")
|
||||||
|
assert isinstance(provider, AtlasProvider)
|
||||||
|
assert provider.provider_type == ProviderType.ATLAS
|
||||||
|
|
||||||
def test_get_provider_by_enum(self):
|
def test_get_provider_by_enum(self):
|
||||||
provider = get_provider(ProviderType.CLAUDE)
|
provider = get_provider(ProviderType.CLAUDE)
|
||||||
assert isinstance(provider, ClaudeProvider)
|
assert isinstance(provider, ClaudeProvider)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user