From e7ec981f79a5dbd96a28f31a304dda36c3b881f4 Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:16:22 +0800 Subject: [PATCH] test(providers): verify Eden AI gateway contract --- nanobot/config/schema.py | 2 +- nanobot/providers/registry.py | 5 +- tests/providers/test_edenai_provider.py | 71 +++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 tests/providers/test_edenai_provider.py diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 098b93efe..965f0514c 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -269,7 +269,7 @@ class ProvidersConfig(Base): ant_ling: ProviderConfig = Field(default_factory=ProviderConfig) # Ant Ling aihubmix: ProviderConfig = Field(default_factory=ProviderConfig) # AiHubMix API gateway siliconflow: ProviderConfig = Field(default_factory=ProviderConfig) # SiliconFlow (硅基流动) - edenai: ProviderConfig = Field(default_factory=ProviderConfig) # Eden AI API gateway (EU, OpenAI-compatible) + edenai: ProviderConfig = Field(default_factory=ProviderConfig) # Eden AI API gateway novita: ProviderConfig = Field(default_factory=ProviderConfig) # Novita AI volcengine: ProviderConfig = Field(default_factory=ProviderConfig) # VolcEngine (火山引擎) volcengine_coding_plan: ProviderConfig = Field(default_factory=ProviderConfig) # VolcEngine Coding Plan diff --git a/nanobot/providers/registry.py b/nanobot/providers/registry.py index 4e96ea416..5318dcf72 100644 --- a/nanobot/providers/registry.py +++ b/nanobot/providers/registry.py @@ -196,18 +196,17 @@ PROVIDERS: tuple[ProviderSpec, ...] = ( supports_prompt_caching=True, gateway_reasoning_style="reasoning_effort", ), - # Eden AI: EU-hosted, OpenAI-compatible gateway. Models use the "provider/model" + # Eden AI: OpenAI-compatible gateway. Models use the "provider/model" # naming scheme (e.g. "anthropic/claude-sonnet-4-5"); the full id is sent upstream. ProviderSpec( name="edenai", - keywords=("edenai", "eden"), + keywords=("edenai",), env_key="EDENAI_API_KEY", display_name="Eden AI", backend="openai_compat", is_gateway=True, detect_by_base_keyword="edenai", default_api_base="https://api.edenai.run/v3", - gateway_reasoning_style="reasoning_effort", ), # OpenCode Zen: OpenAI-compatible chat-completions gateway for coding models. # models.dev/OpenCode use provider id "opencode" and model ids like diff --git a/tests/providers/test_edenai_provider.py b/tests/providers/test_edenai_provider.py new file mode 100644 index 000000000..56dc14f89 --- /dev/null +++ b/tests/providers/test_edenai_provider.py @@ -0,0 +1,71 @@ +"""Tests for the Eden AI provider registration.""" + +from unittest.mock import patch + +from nanobot.config.schema import Config, ProvidersConfig +from nanobot.providers.openai_compat_provider import OpenAICompatProvider +from nanobot.providers.registry import PROVIDERS, find_by_name + + +def test_edenai_config_field_exists() -> None: + assert hasattr(ProvidersConfig(), "edenai") + + +def test_edenai_registry_contract() -> None: + specs = {spec.name: spec for spec in PROVIDERS} + + assert "edenai" in specs + edenai = specs["edenai"] + assert edenai.backend == "openai_compat" + assert edenai.env_key == "EDENAI_API_KEY" + assert edenai.display_name == "Eden AI" + assert edenai.is_gateway is True + assert edenai.detect_by_base_keyword == "edenai" + assert edenai.default_api_base == "https://api.edenai.run/v3" + assert edenai.strip_model_prefix is False + # Eden accepts OpenAI's top-level reasoning_effort parameter. Do not add + # OpenRouter's separate {"reasoning": {"effort": ...}} request shape. + assert edenai.gateway_reasoning_style == "" + + +def test_edenai_forced_provider_uses_default_api_base() -> None: + config = Config.model_validate( + { + "providers": {"edenai": {"apiKey": "eden-key"}}, + "agents": { + "defaults": { + "provider": "edenai", + "model": "anthropic/claude-sonnet-4-5", + } + }, + } + ) + + model = "anthropic/claude-sonnet-4-5" + assert config.get_provider_name(model) == "edenai" + assert config.get_api_key(model) == "eden-key" + assert config.get_api_base(model) == "https://api.edenai.run/v3" + + +def test_edenai_preserves_model_id_and_reasoning_effort() -> None: + spec = find_by_name("edenai") + with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"): + provider = OpenAICompatProvider( + api_key="eden-key", + default_model="anthropic/claude-sonnet-4-5", + spec=spec, + ) + + kwargs = provider._build_kwargs( + messages=[{"role": "user", "content": "hi"}], + tools=None, + model="anthropic/claude-sonnet-4-5", + max_tokens=1024, + temperature=0.7, + reasoning_effort="medium", + tool_choice=None, + ) + + assert kwargs["model"] == "anthropic/claude-sonnet-4-5" + assert kwargs["reasoning_effort"] == "medium" + assert "reasoning" not in kwargs.get("extra_body", {})