mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-22 00:31:44 +00:00
feat(provider): add Eden AI as an OpenAI-compatible provider
Adds Eden AI (https://www.edenai.co/) as a JSON-configured provider in litellm/llms/openai_like/providers.json, the matching LlmProviders enum entry, the .env.example documentation block, and unit + live tests under tests/test_litellm/llms/openai_like/. Eden AI is an aggregator exposing 100+ underlying models from OpenAI, Anthropic, Google, Mistral and others through a single OpenAI-shape chat-completions endpoint. It is a clean drop-in: standard request body, standard `choices[0].message` response, OpenAI-shape `usage`, standard SSE streaming, and standard `tool_calls`. No Python adapter is needed. Files: - litellm/llms/openai_like/providers.json: register `edenai` (base URL https://api.edenai.run/v3, env var EDENAI_API_KEY, optional EDENAI_API_BASE override, max_completion_tokens -> max_tokens param mapping) - litellm/types/utils.py: LlmProviders.EDENAI = "edenai" - litellm/constants.py: edenai added to LITELLM_CHAT_PROVIDERS, openai_compatible_providers, and the base URL added to openai_compatible_endpoints (mirroring Helicone PR #17663) - provider_endpoints_support.json: edenai entry under "providers" so the check_provider_folders_documented.py CI gate sees the new openai_like provider as documented; chat_completions and messages are true (the openai_like loader's default surface plus LiteLLM's internal Anthropic-format -> OpenAI translation); responses, embeddings, image_generations, audio_transcriptions, audio_speech, moderations, batches, rerank are false (Eden AI's API supports them but LiteLLM-side handlers under litellm/llms/edenai/ ship in follow-up PRs and will flip those flags as they land) - README.md: row in the Supported Providers table; chat / messages ticked, matching what is wired through the openai_like loader today - .env.example: document EDENAI_API_KEY and EDENAI_API_BASE next to the existing provider blocks - tests/test_litellm/llms/openai_like/test_edenai.py: mocked tests for enum membership, JSON config shape, get_llm_provider routing, EDENAI_API_BASE override, Router config, mocked completion; plus a live integration test guarded by `pytest.skipif(not os.environ.get('EDENAI_API_KEY'))` that calls api.edenai.run/v3 end-to-end and validates the response shape Endpoints wired through this PR: /chat/completions only (via the openai_like loader's default surface). /messages is also routed via LiteLLM's internal Anthropic-format translation; /responses is NOT (would require explicit `supported_endpoints` opt-in in the JSON config). Provider-specific handlers under litellm/llms/edenai/ for /responses, /embeddings, /image/generations, /audio/* and /moderations ship in follow-up PRs. Companion docs PR: BerriAI/litellm-docs adds the docs/providers/edenai.md page that the README and provider_endpoints_support.json link to. Per CONTRIBUTING.md: - scope is isolated (one provider, no drive-by changes) - adds 1 mocked test in tests/test_litellm/ (hard requirement) plus a live integration test guarded by EDENAI_API_KEY presence - passes make lint and make test-unit - CLA: https://cla-assistant.io/BerriAI/litellm Pattern matches Helicone (#17663) and AIHubMix (#24294) — closest JSON-configured aggregator analogs.
This commit is contained in:
parent
98cd057f38
commit
4854eebded
7 changed files with 206 additions and 0 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -298,6 +298,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) | ✅ | ✅ | ✅ | | ✅ | | | | | |
|
||||
|
|
|
|||
|
|
@ -552,6 +552,7 @@ LITELLM_CHAT_PROVIDERS = [
|
|||
"huggingface",
|
||||
"together_ai",
|
||||
"datarobot",
|
||||
"edenai",
|
||||
"helicone",
|
||||
"openrouter",
|
||||
"cometapi",
|
||||
|
|
@ -780,6 +781,7 @@ openai_compatible_endpoints: List = [
|
|||
"https://api.lambda.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",
|
||||
|
|
@ -819,6 +821,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
|
||||
"apertis", # Apertis - JSON-configured provider
|
||||
"nano-gpt", # Nano-GPT - JSON-configured provider
|
||||
|
|
|
|||
|
|
@ -114,5 +114,13 @@
|
|||
"param_mappings": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3333,6 +3333,7 @@ class LlmProviders(str, Enum):
|
|||
LITELLM_AGENT = "litellm_agent"
|
||||
CURSOR = "cursor"
|
||||
BEDROCK_MANTLE = "bedrock_mantle"
|
||||
EDENAI = "edenai"
|
||||
|
||||
|
||||
# Create a set of all provider values for quick lookup
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
},
|
||||
"elevenlabs": {
|
||||
"display_name": "ElevenLabs (`elevenlabs`)",
|
||||
"url": "https://docs.litellm.ai/docs/providers/elevenlabs",
|
||||
|
|
|
|||
174
tests/test_litellm/llms/openai_like/test_edenai.py
Normal file
174
tests/test_litellm/llms/openai_like/test_edenai.py
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue