diff --git a/README.md b/README.md index 41248556..030703b7 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ A-Evolve is a **framework**, not a standalone agent. Every axis is pluggable: | **Agent (BYOA)** | `BaseAgent.solve()` | Any agent architecture — ReAct, Plan-and-Solve, custom | `SweAgent`, `McpAgent` | | **Benchmark (BYOE)** | `BenchmarkAdapter.get_tasks()` / `.evaluate()` | Any domain with task + evaluation signal | SWE-bench, MCP-Atlas, Terminal-Bench 2.0, SkillsBench, CL-bench | | **Algorithm (BYO-Algo)** | `EvolutionEngine.step()` | Any evolution strategy | `AEvolveEngine` (LLM-driven mutation) | -| **LLM Provider** | `LLMProvider.complete()` | Any model API | Anthropic, OpenAI, AWS Bedrock | +| **LLM Provider** | `LLMProvider.complete()` | Any model API | Anthropic, OpenAI, Atlas Cloud, AWS Bedrock | ### Built-in Evolution Algorithms diff --git a/agent_evolve/algorithms/adaptive_evolve/engine.py b/agent_evolve/algorithms/adaptive_evolve/engine.py index 71a829ab..62751be7 100644 --- a/agent_evolve/algorithms/adaptive_evolve/engine.py +++ b/agent_evolve/algorithms/adaptive_evolve/engine.py @@ -81,6 +81,16 @@ def bash(command: str) -> str: def _create_default_llm(config: EvolveConfig) -> LLMProvider: """Create default LLM provider based on config.""" model = config.evolver_model + from ...llm.model_resolver import is_atlascloud_model, strip_atlascloud_prefix + + if is_atlascloud_model(model): + from ...llm.atlascloud import AtlasCloudProvider + + return AtlasCloudProvider( + model=strip_atlascloud_prefix(model), + api_key=config.extra.get("atlascloud_api_key"), + base_url=config.extra.get("atlascloud_api_base"), + ) if "." in model and ("anthropic" in model or "amazon" in model or "meta" in model): from ...llm.bedrock import BedrockProvider return BedrockProvider(model_id=model, region=config.extra.get("region", "us-west-2")) diff --git a/agent_evolve/algorithms/adaptive_skill/tools.py b/agent_evolve/algorithms/adaptive_skill/tools.py index ad518783..12b851fc 100644 --- a/agent_evolve/algorithms/adaptive_skill/tools.py +++ b/agent_evolve/algorithms/adaptive_skill/tools.py @@ -52,6 +52,16 @@ def bash(command: str) -> str: def create_default_llm(config: EvolveConfig) -> LLMProvider: """Create the default LLM provider based on the evolver_model config string.""" model = config.evolver_model + from ...llm.model_resolver import is_atlascloud_model, strip_atlascloud_prefix + + if is_atlascloud_model(model): + from ...llm.atlascloud import AtlasCloudProvider + + return AtlasCloudProvider( + model=strip_atlascloud_prefix(model), + api_key=config.extra.get("atlascloud_api_key"), + base_url=config.extra.get("atlascloud_api_base"), + ) if "." in model and ("anthropic" in model or "amazon" in model or "meta" in model): from ...llm.bedrock import BedrockProvider diff --git a/agent_evolve/algorithms/navigation/_evolver_tools.py b/agent_evolve/algorithms/navigation/_evolver_tools.py index d0949f83..201d1d70 100644 --- a/agent_evolve/algorithms/navigation/_evolver_tools.py +++ b/agent_evolve/algorithms/navigation/_evolver_tools.py @@ -322,6 +322,16 @@ def make_workspace_bash( def create_default_llm(config: EvolveConfig) -> LLMProvider: """Create the default LLM provider based on the evolver_model config string.""" model = config.evolver_model + from ...llm.model_resolver import is_atlascloud_model, strip_atlascloud_prefix + + if is_atlascloud_model(model): + from ...llm.atlascloud import AtlasCloudProvider + + return AtlasCloudProvider( + model=strip_atlascloud_prefix(model), + api_key=config.extra.get("atlascloud_api_key"), + base_url=config.extra.get("atlascloud_api_base"), + ) if "." in model and ("anthropic" in model or "amazon" in model or "meta" in model): from ...llm.bedrock import BedrockProvider diff --git a/agent_evolve/algorithms/skillforge/tools.py b/agent_evolve/algorithms/skillforge/tools.py index ad518783..12b851fc 100644 --- a/agent_evolve/algorithms/skillforge/tools.py +++ b/agent_evolve/algorithms/skillforge/tools.py @@ -52,6 +52,16 @@ def bash(command: str) -> str: def create_default_llm(config: EvolveConfig) -> LLMProvider: """Create the default LLM provider based on the evolver_model config string.""" model = config.evolver_model + from ...llm.model_resolver import is_atlascloud_model, strip_atlascloud_prefix + + if is_atlascloud_model(model): + from ...llm.atlascloud import AtlasCloudProvider + + return AtlasCloudProvider( + model=strip_atlascloud_prefix(model), + api_key=config.extra.get("atlascloud_api_key"), + base_url=config.extra.get("atlascloud_api_base"), + ) if "." in model and ("anthropic" in model or "amazon" in model or "meta" in model): from ...llm.bedrock import BedrockProvider diff --git a/agent_evolve/algorithms/unified/operators/llm_bash_evolve.py b/agent_evolve/algorithms/unified/operators/llm_bash_evolve.py index ee35fb06..9f746305 100644 --- a/agent_evolve/algorithms/unified/operators/llm_bash_evolve.py +++ b/agent_evolve/algorithms/unified/operators/llm_bash_evolve.py @@ -90,6 +90,21 @@ def bash(command: str) -> str: def _resolve_llm(model: str, region: str): + if model.startswith(("atlascloud:", "atlascloud/")): + from ..openai_compat import OpenAICompatProvider + + model_id = model.removeprefix("atlascloud:").removeprefix("atlascloud/") + return ( + OpenAICompatProvider( + model=model_id or "qwen/qwen3.5-flash", + api_key=os.environ.get("ATLASCLOUD_API_KEY") + or os.environ.get("ATLAS_CLOUD_API_KEY"), + base_url=os.environ.get("ATLASCLOUD_API_BASE") + or os.environ.get("ATLAS_CLOUD_API_BASE") + or "https://api.atlascloud.ai/v1", + ), + "atlascloud", + ) if ( model.startswith("openai:") or model.startswith("/") diff --git a/agent_evolve/llm/atlascloud.py b/agent_evolve/llm/atlascloud.py new file mode 100644 index 00000000..fd7b1024 --- /dev/null +++ b/agent_evolve/llm/atlascloud.py @@ -0,0 +1,37 @@ +"""Atlas Cloud OpenAI-compatible LLM provider.""" + +from __future__ import annotations + +import os + +from .openai import OpenAIProvider + +ATLAS_CLOUD_API_BASE = "https://api.atlascloud.ai/v1" +ATLAS_CLOUD_DEFAULT_MODEL = "qwen/qwen3.5-flash" + + +class AtlasCloudProvider(OpenAIProvider): + """LLM provider using Atlas Cloud's OpenAI-compatible chat API.""" + + def __init__( + self, + model: str = ATLAS_CLOUD_DEFAULT_MODEL, + api_key: str | None = None, + base_url: str | None = None, + ): + resolved_api_key = ( + api_key + or os.environ.get("ATLASCLOUD_API_KEY") + or os.environ.get("ATLAS_CLOUD_API_KEY") + ) + resolved_base_url = ( + base_url + or os.environ.get("ATLASCLOUD_API_BASE") + or os.environ.get("ATLAS_CLOUD_API_BASE") + or ATLAS_CLOUD_API_BASE + ) + super().__init__( + model=model, + api_key=resolved_api_key, + base_url=resolved_base_url, + ) diff --git a/agent_evolve/llm/model_resolver.py b/agent_evolve/llm/model_resolver.py new file mode 100644 index 00000000..e56bab5d --- /dev/null +++ b/agent_evolve/llm/model_resolver.py @@ -0,0 +1,19 @@ +"""Helpers for resolving configured LLM model strings.""" + +from __future__ import annotations + +ATLAS_CLOUD_PREFIXES = ("atlascloud:", "atlascloud/") +ATLAS_CLOUD_DEFAULT_MODEL = "qwen/qwen3.5-flash" + + +def is_atlascloud_model(model: str) -> bool: + """Return whether a model string requests Atlas Cloud explicitly.""" + return model.startswith(ATLAS_CLOUD_PREFIXES) + + +def strip_atlascloud_prefix(model: str) -> str: + """Return the Atlas Cloud model id after removing the provider prefix.""" + for prefix in ATLAS_CLOUD_PREFIXES: + if model.startswith(prefix): + return model.removeprefix(prefix) or ATLAS_CLOUD_DEFAULT_MODEL + return model diff --git a/agent_evolve/llm/openai.py b/agent_evolve/llm/openai.py index 2c7a286d..ed5ccbe9 100644 --- a/agent_evolve/llm/openai.py +++ b/agent_evolve/llm/openai.py @@ -10,14 +10,25 @@ class OpenAIProvider(LLMProvider): """LLM provider using the OpenAI API (GPT / o-series models).""" - def __init__(self, model: str = "gpt-4o", api_key: str | None = None): + def __init__( + self, + model: str = "gpt-4o", + api_key: str | None = None, + base_url: str | None = None, + ): try: import openai except ImportError: raise ImportError("pip install openai (or: pip install agent-evolve[openai])") self.model = model - self.client = openai.OpenAI(api_key=api_key) if api_key else openai.OpenAI() + kwargs: dict[str, str] = {} + if api_key: + kwargs["api_key"] = api_key + if base_url: + kwargs["base_url"] = base_url + kwargs.setdefault("api_key", "EMPTY") + self.client = openai.OpenAI(**kwargs) def complete( self, diff --git a/tests/test_atlascloud_provider.py b/tests/test_atlascloud_provider.py new file mode 100644 index 00000000..78c04def --- /dev/null +++ b/tests/test_atlascloud_provider.py @@ -0,0 +1,58 @@ +"""Tests for Atlas Cloud provider resolution.""" + +from __future__ import annotations + +import sys +import types + +from agent_evolve.algorithms.adaptive_skill.tools import create_default_llm as create_skill_llm +from agent_evolve.config import EvolveConfig +from agent_evolve.llm.model_resolver import is_atlascloud_model, strip_atlascloud_prefix + + +class FakeOpenAI: + def __init__(self, **kwargs): + self.kwargs = kwargs + self.chat = types.SimpleNamespace( + completions=types.SimpleNamespace(create=lambda **_: None) + ) + + +def test_atlascloud_model_prefix_helpers(): + assert is_atlascloud_model("atlascloud:qwen/qwen3.5-flash") + assert is_atlascloud_model("atlascloud/deepseek-ai/deepseek-v4-pro") + assert not is_atlascloud_model("gpt-4o") + assert strip_atlascloud_prefix("atlascloud:qwen/qwen3.5-flash") == "qwen/qwen3.5-flash" + assert ( + strip_atlascloud_prefix("atlascloud/deepseek-ai/deepseek-v4-pro") + == "deepseek-ai/deepseek-v4-pro" + ) + + +def test_atlascloud_provider_uses_openai_compatible_endpoint(monkeypatch): + fake_module = types.SimpleNamespace(OpenAI=FakeOpenAI) + monkeypatch.setitem(sys.modules, "openai", fake_module) + + from agent_evolve.llm.atlascloud import AtlasCloudProvider + + provider = AtlasCloudProvider(model="qwen/qwen3.5-flash", api_key="ac-test") + + assert provider.model == "qwen/qwen3.5-flash" + assert provider.client.kwargs == { + "api_key": "ac-test", + "base_url": "https://api.atlascloud.ai/v1", + } + + +def test_default_llm_factory_resolves_atlascloud(monkeypatch): + fake_module = types.SimpleNamespace(OpenAI=FakeOpenAI) + monkeypatch.setitem(sys.modules, "openai", fake_module) + monkeypatch.setenv("ATLASCLOUD_API_KEY", "ac-env") + + provider = create_skill_llm( + EvolveConfig(evolver_model="atlascloud:deepseek-ai/deepseek-v4-pro") + ) + + assert provider.model == "deepseek-ai/deepseek-v4-pro" + assert provider.client.kwargs["api_key"] == "ac-env" + assert provider.client.kwargs["base_url"] == "https://api.atlascloud.ai/v1"