From 76370135872fd537f1151eb95bfe4f47d4332d91 Mon Sep 17 00:00:00 2001 From: "Victor M. SMITH" <72023257+MVS-source@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:29:35 +0200 Subject: [PATCH 1/3] feat(provider): add Eden AI as an OpenAI-compatible provider Eden AI (https://www.edenai.co) is an OpenAI-compatible aggregator exposing 100+ models from many providers through a single EU-hosted endpoint and API key. Registers 'edenai' as an OpenAI-compatible provider via the existing openai_like loader (providers.json + LlmProviders enum + constants + endpoint support), with tests and docs. Model naming uses the provider/model format. Supersedes #27471 (original branch is on a fork that is no longer maintained). API key via EDENAI_API_KEY only. Signed-off-by: Victor M. SMITH <72023257+MVS-source@users.noreply.github.com> --- .env.example | 3 + README.md | 1 + litellm/constants.py | 3 + litellm/llms/openai_like/providers.json | 8 + litellm/types/utils.py | 1 + provider_endpoints_support.json | 16 ++ .../llms/openai_like/test_edenai.py | 174 ++++++++++++++++++ 7 files changed, 206 insertions(+) create mode 100644 tests/test_litellm/llms/openai_like/test_edenai.py diff --git a/.env.example b/.env.example index 24c2b608414..17dd9fe9840 100644 --- a/.env.example +++ b/.env.example @@ -24,6 +24,9 @@ INFISICAL_TOKEN = "" NOVITA_API_KEY = "" # INFINITY INFINITY_API_KEY = "" +# Eden AI +EDENAI_API_KEY = "" +EDENAI_API_BASE = "https://api.edenai.run/v3" # Development Configs LITELLM_MASTER_KEY = "sk-1234" diff --git a/README.md b/README.md index 90d3e944fcc..8735292cebd 100644 --- a/README.md +++ b/README.md @@ -304,6 +304,7 @@ curl -X POST 'http://0.0.0.0:4000/v1/chat/completions' \ | [Deepgram (`deepgram`)](https://docs.litellm.ai/docs/providers/deepgram) | ✅ | ✅ | ✅ | | | ✅ | | | | | | [DeepInfra (`deepinfra`)](https://docs.litellm.ai/docs/providers/deepinfra) | ✅ | ✅ | ✅ | | | | | | | | | [Deepseek (`deepseek`)](https://docs.litellm.ai/docs/providers/deepseek) | ✅ | ✅ | ✅ | | | | | | | | +| [Eden AI (`edenai`)](https://docs.litellm.ai/docs/providers/edenai) | ✅ | ✅ | | | | | | | | | | [ElevenLabs (`elevenlabs`)](https://docs.litellm.ai/docs/providers/elevenlabs) | ✅ | ✅ | ✅ | | | ✅ | ✅ | | | | | [Empower (`empower`)](https://docs.litellm.ai/docs/providers/empower) | ✅ | ✅ | ✅ | | | | | | | | | [Fal AI (`fal_ai`)](https://docs.litellm.ai/docs/providers/fal_ai) | ✅ | ✅ | ✅ | | ✅ | | | | | | diff --git a/litellm/constants.py b/litellm/constants.py index 7423d9b2211..ac70fa57440 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -473,6 +473,7 @@ LITELLM_CHAT_PROVIDERS = [ "huggingface", "together_ai", "datarobot", + "edenai", "helicone", "openrouter", "cometapi", @@ -710,6 +711,7 @@ openai_compatible_endpoints: List = [ "https://api.inceptionlabs.ai/v1", "https://api.hyperbolic.xyz/v1", "https://ai-gateway.helicone.ai/", + "https://api.edenai.run/v3", "https://ai-gateway.vercel.sh/v1", "https://api.inference.wandb.ai/v1", "https://api.clarifai.com/v2/ext/openai/v1", @@ -752,6 +754,7 @@ openai_compatible_providers: List = [ "novita", "meta_llama", "publicai", # PublicAI - JSON-configured provider + "edenai", # Eden AI - JSON-configured meta-gateway provider "synthetic", # Synthetic - JSON-configured provider "tensormesh", # Tensormesh - JSON-configured provider "apertis", # Apertis - JSON-configured provider diff --git a/litellm/llms/openai_like/providers.json b/litellm/llms/openai_like/providers.json index d87346fea70..98c9ea50bb0 100644 --- a/litellm/llms/openai_like/providers.json +++ b/litellm/llms/openai_like/providers.json @@ -115,6 +115,14 @@ "max_completion_tokens": "max_tokens" } }, + "edenai": { + "base_url": "https://api.edenai.run/v3", + "api_key_env": "EDENAI_API_KEY", + "api_base_env": "EDENAI_API_BASE", + "param_mappings": { + "max_completion_tokens": "max_tokens" + } + }, "darkbloom": { "base_url": "https://api.darkbloom.dev/v1", "api_key_env": "DARKBLOOM_API_KEY", diff --git a/litellm/types/utils.py b/litellm/types/utils.py index 908f5b76424..63afdc9497c 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -3384,6 +3384,7 @@ class LlmProviders(str, Enum): LITELLM_AGENT = "litellm_agent" CURSOR = "cursor" BEDROCK_MANTLE = "bedrock_mantle" + EDENAI = "edenai" GDC = "gdc" diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 3034ada56ba..85b2cbcc1fd 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -831,6 +831,22 @@ "search": true } }, + "edenai": { + "display_name": "Eden AI (`edenai`)", + "url": "https://docs.litellm.ai/docs/providers/edenai", + "endpoints": { + "chat_completions": true, + "messages": true, + "responses": false, + "embeddings": false, + "image_generations": false, + "audio_transcriptions": false, + "audio_speech": false, + "moderations": false, + "batches": false, + "rerank": false + } + }, "e2b": { "display_name": "E2B (`e2b`)", "url": "https://docs.litellm.ai/docs/providers/e2b", diff --git a/tests/test_litellm/llms/openai_like/test_edenai.py b/tests/test_litellm/llms/openai_like/test_edenai.py new file mode 100644 index 00000000000..e644ac86818 --- /dev/null +++ b/tests/test_litellm/llms/openai_like/test_edenai.py @@ -0,0 +1,174 @@ +""" +Tests for Eden AI provider configuration and integration. + +Eden AI (https://www.edenai.co/) is an OpenAI-compatible aggregator that +exposes 100+ underlying models from OpenAI, Anthropic, Google, Mistral and +others through a single chat-completions endpoint. +""" + +import os + +import pytest + +import litellm + + +class TestEdenAIProviderConfig: + """Test Eden AI provider configuration""" + + def test_edenai_in_provider_list(self): + """Test that edenai is in the provider list""" + from litellm import LlmProviders + + assert hasattr(LlmProviders, "EDENAI") + assert LlmProviders.EDENAI.value == "edenai" + assert "edenai" in litellm.provider_list + + def test_edenai_json_config_exists(self): + """Test that edenai is configured in providers.json""" + from litellm.llms.openai_like.json_loader import JSONProviderRegistry + + assert JSONProviderRegistry.exists("edenai") + + edenai = JSONProviderRegistry.get("edenai") + assert edenai is not None + assert edenai.base_url == "https://api.edenai.run/v3" + assert edenai.api_key_env == "EDENAI_API_KEY" + assert edenai.api_base_env == "EDENAI_API_BASE" + assert edenai.param_mappings.get("max_completion_tokens") == "max_tokens" + + def test_edenai_provider_resolution(self): + """Test that provider resolution finds edenai for prefixed model ids""" + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + model, provider, api_key, api_base = get_llm_provider( + model="edenai/openai/gpt-4o-mini", + custom_llm_provider=None, + api_base=None, + api_key=None, + ) + + # Eden AI's model ids are themselves prefixed (e.g. openai/gpt-4o-mini), + # so the model returned is everything after the leading "edenai/". + assert model == "openai/gpt-4o-mini" + assert provider == "edenai" + assert api_base == "https://api.edenai.run/v3" + + def test_edenai_api_base_env_override(self, monkeypatch): + """Test that EDENAI_API_BASE overrides the default base URL""" + from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider + + monkeypatch.setenv("EDENAI_API_BASE", "https://custom-edenai.example.com/v3") + _, provider, _, api_base = get_llm_provider( + model="edenai/openai/gpt-4o-mini", + custom_llm_provider=None, + api_base=None, + api_key=None, + ) + assert provider == "edenai" + assert api_base == "https://custom-edenai.example.com/v3" + + def test_edenai_router_config(self): + """Test that edenai can be used in Router configuration""" + from litellm import Router + + router = Router( + model_list=[ + { + "model_name": "edenai-gpt-4o-mini", + "litellm_params": { + "model": "edenai/openai/gpt-4o-mini", + "api_key": "test-key", + }, + } + ] + ) + + assert len(router.model_list) == 1 + assert router.model_list[0]["model_name"] == "edenai-gpt-4o-mini" + + +class TestEdenAICompletionMocked: + """Mocked completion tests — no real API calls.""" + + def test_edenai_completion_mocked(self): + """Test that a mocked completion through the edenai/ prefix routes + correctly and returns the stub content unchanged.""" + response = litellm.completion( + model="edenai/openai/gpt-4o-mini", + messages=[{"role": "user", "content": "Say PING and nothing else"}], + api_key="test-key", + mock_response="PING", + ) + + assert response is not None + assert hasattr(response, "choices") + assert len(response.choices) > 0 + assert response.choices[0].message.content == "PING" + + +class TestEdenAILiveIntegration: + """Live integration tests against the real Eden AI API. + + Skipped automatically when EDENAI_API_KEY is not set, so CI without the + secret stays green. When the key is present (local dev, or a dedicated + integration runner) these validate that each endpoint flagged `true` in + provider_endpoints_support.json actually routes a request through to + api.edenai.run/v3 end-to-end. Endpoints flagged `false` (`/responses`, + `/embeddings`, `/image/generations`, `/audio/*`, `/moderations`, + `/batches`, `/rerank`) intentionally have no live test here; their + handlers ship in follow-up PRs and the tests come with them. + """ + + @pytest.mark.skipif( + not os.environ.get("EDENAI_API_KEY"), reason="EDENAI_API_KEY not set" + ) + def test_edenai_live_chat_completions(self): + """`/v1/chat/completions` via litellm.completion() — the openai_like + loader's primary surface.""" + response = litellm.completion( + model="edenai/openai/gpt-4o-mini", + messages=[ + { + "role": "user", + "content": "Reply with 'PING' and nothing else.", + } + ], + max_tokens=8, + ) + + assert response is not None + assert response.choices[0].message.content + assert response.usage is not None + assert response.usage.total_tokens > 0 + + @pytest.mark.skipif( + not os.environ.get("EDENAI_API_KEY"), reason="EDENAI_API_KEY not set" + ) + @pytest.mark.asyncio + async def test_edenai_live_anthropic_messages(self): + """`/v1/messages` (Anthropic-format) via litellm.anthropic_messages(). + LiteLLM translates the Anthropic-shape request to OpenAI shape + internally and routes through Eden AI's openai-compatible endpoint; + the response is translated back to Anthropic shape before being + returned to the caller.""" + response = await litellm.anthropic_messages( + model="edenai/anthropic/claude-opus-4-6", + messages=[ + { + "role": "user", + "content": "Reply with 'MSG' and nothing else.", + } + ], + max_tokens=8, + ) + + assert response is not None + # anthropic_messages returns a dict (Anthropic-shape response) + assert response.get("type") == "message" + assert response.get("role") == "assistant" + content = response.get("content", []) + assert content and content[0].get("type") == "text" + assert content[0].get("text") + usage = response.get("usage", {}) + assert usage.get("total_tokens", 0) > 0 From d35bc330eb9fcfac65ee1a3296cfc423af96cf31 Mon Sep 17 00:00:00 2001 From: "Victor M. SMITH" <72023257+MVS-source@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:32:22 +0200 Subject: [PATCH 2/3] test(edenai): move live integration tests out of the unit suite Per review: keep only unit/mocked tests here; live API tests move to tests/e2e/test_edenai_e2e.py. --- .../llms/openai_like/test_edenai.py | 67 ------------------- 1 file changed, 67 deletions(-) diff --git a/tests/test_litellm/llms/openai_like/test_edenai.py b/tests/test_litellm/llms/openai_like/test_edenai.py index e644ac86818..9f8e1f265d9 100644 --- a/tests/test_litellm/llms/openai_like/test_edenai.py +++ b/tests/test_litellm/llms/openai_like/test_edenai.py @@ -105,70 +105,3 @@ class TestEdenAICompletionMocked: assert hasattr(response, "choices") assert len(response.choices) > 0 assert response.choices[0].message.content == "PING" - - -class TestEdenAILiveIntegration: - """Live integration tests against the real Eden AI API. - - Skipped automatically when EDENAI_API_KEY is not set, so CI without the - secret stays green. When the key is present (local dev, or a dedicated - integration runner) these validate that each endpoint flagged `true` in - provider_endpoints_support.json actually routes a request through to - api.edenai.run/v3 end-to-end. Endpoints flagged `false` (`/responses`, - `/embeddings`, `/image/generations`, `/audio/*`, `/moderations`, - `/batches`, `/rerank`) intentionally have no live test here; their - handlers ship in follow-up PRs and the tests come with them. - """ - - @pytest.mark.skipif( - not os.environ.get("EDENAI_API_KEY"), reason="EDENAI_API_KEY not set" - ) - def test_edenai_live_chat_completions(self): - """`/v1/chat/completions` via litellm.completion() — the openai_like - loader's primary surface.""" - response = litellm.completion( - model="edenai/openai/gpt-4o-mini", - messages=[ - { - "role": "user", - "content": "Reply with 'PING' and nothing else.", - } - ], - max_tokens=8, - ) - - assert response is not None - assert response.choices[0].message.content - assert response.usage is not None - assert response.usage.total_tokens > 0 - - @pytest.mark.skipif( - not os.environ.get("EDENAI_API_KEY"), reason="EDENAI_API_KEY not set" - ) - @pytest.mark.asyncio - async def test_edenai_live_anthropic_messages(self): - """`/v1/messages` (Anthropic-format) via litellm.anthropic_messages(). - LiteLLM translates the Anthropic-shape request to OpenAI shape - internally and routes through Eden AI's openai-compatible endpoint; - the response is translated back to Anthropic shape before being - returned to the caller.""" - response = await litellm.anthropic_messages( - model="edenai/anthropic/claude-opus-4-6", - messages=[ - { - "role": "user", - "content": "Reply with 'MSG' and nothing else.", - } - ], - max_tokens=8, - ) - - assert response is not None - # anthropic_messages returns a dict (Anthropic-shape response) - assert response.get("type") == "message" - assert response.get("role") == "assistant" - content = response.get("content", []) - assert content and content[0].get("type") == "text" - assert content[0].get("text") - usage = response.get("usage", {}) - assert usage.get("total_tokens", 0) > 0 From b19eb8576b9275f61d63b44d3ad6b3479c8a31f7 Mon Sep 17 00:00:00 2001 From: "Victor M. SMITH" <72023257+MVS-source@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:32:23 +0200 Subject: [PATCH 3/3] test(edenai): add live e2e tests in tests/e2e/ Moved from the unit suite. Fix Anthropic-messages usage assertion: the Anthropic response shape exposes input_tokens/output_tokens, not total_tokens. --- tests/e2e/test_edenai_e2e.py | 80 ++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 tests/e2e/test_edenai_e2e.py diff --git a/tests/e2e/test_edenai_e2e.py b/tests/e2e/test_edenai_e2e.py new file mode 100644 index 00000000000..3480d341fc4 --- /dev/null +++ b/tests/e2e/test_edenai_e2e.py @@ -0,0 +1,80 @@ +"""End-to-end (live) tests for the Eden AI provider. + +These hit the real Eden AI API and are skipped automatically when +EDENAI_API_KEY is not set, so CI without the secret stays green. +Moved here from tests/test_litellm/llms/openai_like/test_edenai.py per +review feedback: live tests belong in tests/e2e/, not the unit suite. +""" + +import os + +import pytest + +import litellm + + +class TestEdenAILiveIntegration: + """Live integration tests against the real Eden AI API. + + Skipped automatically when EDENAI_API_KEY is not set, so CI without the + secret stays green. When the key is present (local dev, or a dedicated + integration runner) these validate that each endpoint flagged `true` in + provider_endpoints_support.json actually routes a request through to + api.edenai.run/v3 end-to-end. Endpoints flagged `false` (`/responses`, + `/embeddings`, `/image/generations`, `/audio/*`, `/moderations`, + `/batches`, `/rerank`) intentionally have no live test here; their + handlers ship in follow-up PRs and the tests come with them. + """ + + @pytest.mark.skipif( + not os.environ.get("EDENAI_API_KEY"), reason="EDENAI_API_KEY not set" + ) + def test_edenai_live_chat_completions(self): + """`/v1/chat/completions` via litellm.completion() — the openai_like + loader's primary surface.""" + response = litellm.completion( + model="edenai/openai/gpt-4o-mini", + messages=[ + { + "role": "user", + "content": "Reply with 'PING' and nothing else.", + } + ], + max_tokens=8, + ) + + assert response is not None + assert response.choices[0].message.content + assert response.usage is not None + assert response.usage.total_tokens > 0 + + @pytest.mark.skipif( + not os.environ.get("EDENAI_API_KEY"), reason="EDENAI_API_KEY not set" + ) + @pytest.mark.asyncio + async def test_edenai_live_anthropic_messages(self): + """`/v1/messages` (Anthropic-format) via litellm.anthropic_messages(). + LiteLLM translates the Anthropic-shape request to OpenAI shape + internally and routes through Eden AI's openai-compatible endpoint; + the response is translated back to Anthropic shape before being + returned to the caller.""" + response = await litellm.anthropic_messages( + model="edenai/anthropic/claude-opus-4-6", + messages=[ + { + "role": "user", + "content": "Reply with 'MSG' and nothing else.", + } + ], + max_tokens=8, + ) + + assert response is not None + # anthropic_messages returns a dict (Anthropic-shape response) + assert response.get("type") == "message" + assert response.get("role") == "assistant" + content = response.get("content", []) + assert content and content[0].get("type") == "text" + assert content[0].get("text") + usage = response.get("usage", {}) + assert usage.get("input_tokens", 0) > 0