diff --git a/.env.example b/.env.example index ff52cbbf..3011c774 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,9 @@ OPENAI_API_KEY= +ATLASCLOUD_API_KEY= ANTHROPIC_API_KEY= VOYAGE_API_KEY= AZURE_EMBED_ENDPOINT= AZURE_EMBED_DEPLOYMENT= AZURE_EMBED_API_VERSION= AZURE_EMBED_API_KEY= -EXA_API_KEY= \ No newline at end of file +EXA_API_KEY= diff --git a/README.md b/README.md index 3d354eea..4e843fe1 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ EvoAgentX is an open-source framework for building, evaluating, and evolving LLM Agents don’t just work—they learn. EvoAgentX improves workflows using self-evolving algorithms. - 🧩 **Plug-and-Play Compatibility** - Easily integrate original [OpenAI](https://github.com/EvoAgentX/EvoAgentX/blob/main/evoagentx/models/openai_model.py) and [qwen](https://github.com/EvoAgentX/EvoAgentX/blob/main/evoagentx/models/aliyun_model.py) or other popular models, including Claude, Deepseek, kimi models through ([LiteLLM](https://github.com/EvoAgentX/EvoAgentX/blob/main/evoagentx/models/litellm_model.py), [siliconflow](https://github.com/EvoAgentX/EvoAgentX/blob/main/evoagentx/models/siliconflow_model.py) or [openrouter](https://github.com/EvoAgentX/EvoAgentX/blob/main/evoagentx/models/openrouter_model.py)). If you want to use LLMs locally deployed on your own machine, you can try LiteLLM. + Easily integrate original [OpenAI](https://github.com/EvoAgentX/EvoAgentX/blob/main/evoagentx/models/openai_model.py) and [qwen](https://github.com/EvoAgentX/EvoAgentX/blob/main/evoagentx/models/aliyun_model.py) or other popular models, including Claude, Deepseek, kimi models through ([LiteLLM](https://github.com/EvoAgentX/EvoAgentX/blob/main/evoagentx/models/litellm_model.py), [siliconflow](https://github.com/EvoAgentX/EvoAgentX/blob/main/evoagentx/models/siliconflow_model.py), [openrouter](https://github.com/EvoAgentX/EvoAgentX/blob/main/evoagentx/models/openrouter_model.py), or [Atlas Cloud](https://www.atlascloud.ai/)). If you want to use LLMs locally deployed on your own machine, you can try LiteLLM. - 🧰 **Comprehensive Built-in Tools** @@ -248,6 +248,20 @@ llm = OpenAILLM(config=openai_config) # Generate a response from the LLM response = llm.generate(prompt="What is Agentic Workflow?") ``` + +Atlas Cloud is available as an optional OpenAI-compatible provider: + +```python +import os +from evoagentx.models import AtlasCloudConfig, AtlasCloudLLM + +llm = AtlasCloudLLM( + config=AtlasCloudConfig( + model="openai/gpt-5.6-luna", + atlascloud_key=os.getenv("ATLASCLOUD_API_KEY"), + ) +) +``` > 📖 More details on supported models and config options: [LLM module guide](./docs/modules/llm.md). diff --git a/evoagentx/models/__init__.py b/evoagentx/models/__init__.py index 1e37a69f..c04ef530 100644 --- a/evoagentx/models/__init__.py +++ b/evoagentx/models/__init__.py @@ -8,3 +8,4 @@ from .openrouter_model import * from .aliyun_model import * from .novita_model import * +from .atlascloud_model import * diff --git a/evoagentx/models/atlascloud_model.py b/evoagentx/models/atlascloud_model.py new file mode 100644 index 00000000..d53038d9 --- /dev/null +++ b/evoagentx/models/atlascloud_model.py @@ -0,0 +1,47 @@ +from openai import AsyncOpenAI, OpenAI +from openai.types.chat import ChatCompletion, ChatCompletionChunk + +from ..core.logging import logger +from ..core.registry import register_model +from .model_configs import AtlasCloudConfig +from .model_utils import Cost, cost_manager +from .openai_model import OpenAILLM + +ATLASCLOUD_BASE_URL = "https://api.atlascloud.ai/v1" + + +@register_model(config_cls=AtlasCloudConfig, alias=["atlascloud"]) +class AtlasCloudLLM(OpenAILLM): + """Atlas Cloud client for its OpenAI-compatible chat-completions API.""" + + def init_model(self): + self._client = None + self._async_client = None + self._default_ignore_fields = [ + "llm_type", + "atlascloud_key", + "output_response", + ] + + def _init_client(self, config: AtlasCloudConfig): + return OpenAI(api_key=config.atlascloud_key, base_url=ATLASCLOUD_BASE_URL) + + def _init_async_client(self, config: AtlasCloudConfig): + return AsyncOpenAI(api_key=config.atlascloud_key, base_url=ATLASCLOUD_BASE_URL) + + def _update_cost(self, response: ChatCompletion | ChatCompletionChunk): + usage = getattr(response, "usage", None) + if usage is None: + logger.warning( + f"[AtlasCloudLLM] usage is missing from response " + f"(id={getattr(response, 'id', '?')}); tokens will not be recorded." + ) + return + cost_manager.update_cost( + cost=Cost( + input_tokens=usage.prompt_tokens, + output_tokens=usage.completion_tokens, + cost=0.0, + ), + model=self.config.model, + ) diff --git a/evoagentx/models/model_configs.py b/evoagentx/models/model_configs.py index ed8a545c..aa6859e2 100644 --- a/evoagentx/models/model_configs.py +++ b/evoagentx/models/model_configs.py @@ -140,6 +140,34 @@ def __str__(self): return self.model +class AtlasCloudConfig(LLMConfig): + + llm_type: str = "AtlasCloudLLM" + atlascloud_key: Optional[str] = Field( + default=None, + description="the API key used to authenticate Atlas Cloud requests", + ) + + # generation parameters + temperature: Optional[float] = Field(default=None, description="the temperature used to scaling logits") + max_tokens: Optional[int] = Field(default=None, description="maximum number of generated tokens") + max_completion_tokens: Optional[int] = Field(default=None, description="An upper bound for the number of tokens that can be generated for a completion, including visible output tokens and reasoning tokens.") + top_p: Optional[float] = Field(default=None, description="Only sample from tokens with cumulative probability greater than top_p when generating text.") + n: Optional[int] = Field(default=None, description="How many chat completion choices to generate for each input message.") + stream: Optional[bool] = Field(default=None, description="If set to true, it sends partial message deltas as they become available.") + stream_options: Optional[dict] = Field(default=None, description="Options for streaming responses.") + timeout: Optional[Union[float, int]] = Field(default=None, description="Timeout in seconds for completion requests.") + + # tools and output format + tools: Optional[List] = Field(default=None, description="A list of tools the model may call.") + tool_choice: Optional[Union[str, dict]] = Field(default=None, description="Controls which tool the model may call.") + parallel_tool_calls: Optional[bool] = Field(default=None, description="Whether to enable parallel function calling during tool use.") + response_format: Optional[Union[BaseModel, dict]] = Field(default=None, description="An object specifying the format that the model must output.") + + def __str__(self): + return self.model + + class NovitaConfig(LLMConfig): # LLM keys diff --git a/tests/src/models/test_atlascloud_model.py b/tests/src/models/test_atlascloud_model.py new file mode 100644 index 00000000..883df34f --- /dev/null +++ b/tests/src/models/test_atlascloud_model.py @@ -0,0 +1,63 @@ +from unittest.mock import MagicMock + +from openai.types.completion_usage import CompletionUsage + +from evoagentx.core.registry import MODEL_REGISTRY +from evoagentx.models.atlascloud_model import ATLASCLOUD_BASE_URL, AtlasCloudLLM +from evoagentx.models.model_configs import AtlasCloudConfig +from evoagentx.models.model_utils import cost_manager + + +def _config() -> AtlasCloudConfig: + return AtlasCloudConfig( + model="openai/gpt-5.6-luna", + atlascloud_key="test-key", + output_response=False, + ) + + +def test_atlascloud_model_is_registered(): + assert MODEL_REGISTRY.get_model("AtlasCloudLLM") is AtlasCloudLLM + assert MODEL_REGISTRY.get_model("atlascloud") is AtlasCloudLLM + assert MODEL_REGISTRY.get_model_config("atlascloud") is AtlasCloudConfig + + +def test_atlascloud_clients_use_the_provider_endpoint(mocker): + sync_client = mocker.patch("evoagentx.models.atlascloud_model.OpenAI") + async_client = mocker.patch("evoagentx.models.atlascloud_model.AsyncOpenAI") + llm = AtlasCloudLLM(config=_config()) + + llm._init_client(llm.config) + llm._init_async_client(llm.config) + + sync_client.assert_called_once_with( + api_key="test-key", base_url=ATLASCLOUD_BASE_URL + ) + async_client.assert_called_once_with( + api_key="test-key", base_url=ATLASCLOUD_BASE_URL + ) + + +def test_atlascloud_credentials_are_not_sent_as_completion_params(): + llm = AtlasCloudLLM(config=_config()) + + assert llm.get_completion_params() == {"model": "openai/gpt-5.6-luna"} + + +def test_atlascloud_records_tokens_without_guessing_cost(): + model = "openai/gpt-5.6-luna" + cost_manager.input_tokens.clear() + cost_manager.output_tokens.clear() + cost_manager.total_tokens.clear() + cost_manager.cost_per_model.clear() + response = MagicMock( + id="atlas-test", + usage=CompletionUsage(prompt_tokens=12, completion_tokens=5, total_tokens=17), + ) + + AtlasCloudLLM(config=_config())._update_cost(response) + + assert cost_manager.input_tokens[model] == 12 + assert cost_manager.output_tokens[model] == 5 + assert cost_manager.total_tokens[model] == 17 + assert cost_manager.cost_per_model[model] == 0.0