This commit is contained in:
Adam Dalloul 2026-10-26 13:12:00 -07:00 • committed by GitHub
commit e9e9e3a15c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 325 additions and 6 deletions

View file

@ -263,6 +263,7 @@ jobs:
tests/unit/llms/e2b
tests/unit/llms/edenai
tests/unit/llms/elevenlabs
tests/unit/llms/empiriolabs
tests/unit/llms/exa_ai
tests/unit/llms/fal_ai
tests/unit/llms/fastcrw

View file

@ -353,6 +353,7 @@ Set `LITELLM_PROXY_API_BASE` and `LITELLM_PROXY_API_KEY` and every model call th
| [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) | ✅ | ✅ | ✅ | | | ✅ | ✅ | | | |
| [EmpirioLabs AI (`empiriolabs`)](https://docs.litellm.ai/docs/providers/empiriolabs) | ✅ | | ✅ | | | | | | | |
| [Empower (`empower`)](https://docs.litellm.ai/docs/providers/empower) | ✅ | ✅ | ✅ | | | | | | | |
| [Fal AI (`fal_ai`)](https://docs.litellm.ai/docs/providers/fal_ai) | ✅ | ✅ | ✅ | | ✅ | | | | | |
| [Featherless AI (`featherless_ai`)](https://docs.litellm.ai/docs/providers/featherless_ai) | ✅ | ✅ | ✅ | | | | | | | |

View file

@ -1735,6 +1735,9 @@ if TYPE_CHECKING:
from .llms.bespoke.decisions.transformation import (
BespokeDecisionsConfig as BespokeDecisionsConfig,
)
from .llms.empiriolabs.decisions.transformation import (
EmpirioLabsDecisionsConfig as EmpirioLabsDecisionsConfig,
)
from .llms.openai.decisions.transformation import (
OpenAIDecisionsConfig as OpenAIDecisionsConfig,
)

View file

@ -172,6 +172,7 @@ LLM_CONFIG_NAMES: Final = (
"HostedVLLMDecisionsConfig",
"LayaDecisionsConfig",
"BespokeDecisionsConfig",
"EmpirioLabsDecisionsConfig",
"OpenAIDecisionsConfig",
"AzureAIDecisionsConfig",
"NvidiaNimRerankConfig",
@ -748,6 +749,7 @@ _LLM_CONFIGS_IMPORT_MAP: Final = {
),
"LayaDecisionsConfig": (".llms.laya.decisions.transformation", "LayaDecisionsConfig"),
"BespokeDecisionsConfig": (".llms.bespoke.decisions.transformation", "BespokeDecisionsConfig"),
"EmpirioLabsDecisionsConfig": (".llms.empiriolabs.decisions.transformation", "EmpirioLabsDecisionsConfig"),
"OpenAIDecisionsConfig": (".llms.openai.decisions.transformation", "OpenAIDecisionsConfig"),
"AzureAIDecisionsConfig": (".llms.azure_ai.decisions.transformation", "AzureAIDecisionsConfig"),
"NvidiaNimRerankConfig": (

View file

@ -0,0 +1,10 @@
from litellm.llms.base_llm.decisions.transformation import BaseDecisionsConfig
class EmpirioLabsDecisionsConfig(BaseDecisionsConfig):
path = "/v1/decisions"
api_key_env = ("EMPIRIOLABS_API_KEY",)
api_base_env = ("EMPIRIOLABS_API_BASE",)
def get_default_api_base(self) -> str | None:
return "https://api.empiriolabs.ai"

View file

@ -1469,6 +1469,34 @@
],
"default_model_placeholder": "gpt-3.5-turbo"
},
{
"provider": "EMPIRIOLABS",
"provider_display_name": "EmpirioLabs AI",
"litellm_provider": "empiriolabs",
"credential_fields": [
{
"key": "api_base",
"label": "API Base",
"placeholder": "https://api.empiriolabs.ai/v1",
"tooltip": null,
"required": false,
"field_type": "text",
"options": null,
"default_value": null
},
{
"key": "api_key",
"label": "API Key",
"placeholder": null,
"tooltip": null,
"required": true,
"field_type": "password",
"options": null,
"default_value": null
}
],
"default_model_placeholder": "empiriolabs/aplomb-1"
},
{
"provider": "EMPOWER",
"provider_display_name": "Empower",

View file

@ -4464,6 +4464,7 @@ class LlmProviders(str, Enum):
STRANDS_DECIDER = "strands_decider"
LAYA = "laya"
BESPOKE = "bespoke"
EMPIRIOLABS = "empiriolabs"
MISTRAL = "mistral"
MILVUS = "milvus"
GROQ = "groq"

View file

@ -9619,6 +9619,8 @@ class ProviderConfigManager:
return litellm.LayaDecisionsConfig()
if provider == LlmProviders.BESPOKE:
return litellm.BespokeDecisionsConfig()
if provider == LlmProviders.EMPIRIOLABS:
return litellm.EmpirioLabsDecisionsConfig()
if provider == LlmProviders.OPENAI:
return litellm.OpenAIDecisionsConfig()
if provider == LlmProviders.AZURE_AI:

View file

@ -3240,7 +3240,7 @@
}
},
"empiriolabs": {
"display_name": "EmpirioLabs (`empiriolabs`)",
"display_name": "EmpirioLabs AI (`empiriolabs`)",
"url": "https://docs.litellm.ai/docs/providers/empiriolabs",
"endpoints": {
"chat_completions": true,
@ -3253,7 +3253,8 @@
"moderations": false,
"batches": false,
"rerank": false,
"a2a": false
"a2a": false,
"systemone": true
}
},
"openinfer": {

View file

@ -139,6 +139,7 @@ _PROVIDERS: Final = (
False,
None,
),
_Provider("empiriolabs", "empiriolabs/aplomb-test", "/v1/decisions", "aplomb-test", _API_KEY, False, None),
)
_PERPLEXITY: Final = _PROVIDERS[0]
_OPENROUTER: Final = next(provider for provider in _PROVIDERS if provider.name == "openrouter")

View file

@ -128,6 +128,7 @@ _PROVIDERS: Final[tuple[tuple[str, str, str, str], ...]] = (
"https://openrouter.ai/api/alpha/decisions",
"typesafe/jev-1.13",
),
("empiriolabs", "empiriolabs/aplomb-1", "https://api.empiriolabs.ai/v1/decisions", "aplomb-1"),
)
_OPENAI_RESPONSE: Final[Mapping[str, object]] = {
@ -1463,7 +1464,12 @@ async def test_databricks_router_deployment_sends_its_own_key_to_its_own_base(
_PROVIDERS_WHOSE_CHAT_MODELS_ALSO_DECIDE: Final = frozenset(
{LlmProviders.OPENAI.value, LlmProviders.OPENROUTER.value, LlmProviders.HOSTED_VLLM.value}
{
LlmProviders.OPENAI.value,
LlmProviders.OPENROUTER.value,
LlmProviders.HOSTED_VLLM.value,
LlmProviders.EMPIRIOLABS.value,
}
)
_DEDICATED_DECIDER_PROVIDERS: Final[tuple[str, ...]] = tuple(
sorted(
@ -3363,6 +3369,147 @@ async def test_fireworks_ai_bills_cached_input_tokens_at_the_cache_read_rate(
assert recording_logger.standard_logging_object["response_cost"] < 300 * float(prices["input_cost_per_token"])
_EMPIRIOLABS_MODEL: Final = "empiriolabs/aplomb-1"
_EMPIRIOLABS_DEFAULT_URL: Final = "https://api.empiriolabs.ai/v1/decisions"
_EMPIRIOLABS_STATE: Final = "I was charged twice for order 4411 and want a refund today."
_EMPIRIOLABS_QUESTIONS: Final[Mapping[str, object]] = MappingProxyType(
{"is_refund": {"type": "noul", "instructions": "Is the customer asking for a refund?"}}
)
_EMPIRIOLABS_RESPONSE: Final[Mapping[str, object]] = {
"model": "aplomb-1",
"answers": {"is_refund": {"type": "noul", "noul": 0.97, "confidence": 0.94}},
"usage": {"input_tokens": 24, "output_tokens": 0},
}
@pytest.mark.parametrize(
("api_base", "environment_api_base", "url"),
(
(None, None, _EMPIRIOLABS_DEFAULT_URL),
(None, "https://environment-gateway.example/v1", "https://environment-gateway.example/v1/decisions"),
(
"https://gateway.example/v1/",
"https://environment-gateway.example/v1",
"https://gateway.example/v1/decisions",
),
("https://gateway.example", None, "https://gateway.example/v1/decisions"),
),
ids=("default_base", "environment_base", "explicit_base_over_environment", "explicit_base_without_v1"),
)
def test_empiriolabs_posts_the_systemone_body_to_v1_decisions_on_the_resolved_base(
api_base: str | None,
environment_api_base: str | None,
url: str,
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.delenv("EMPIRIOLABS_API_BASE", raising=False)
if environment_api_base is not None:
monkeypatch.setenv("EMPIRIOLABS_API_BASE", environment_api_base)
route: Final = respx_mock.post(url).respond(json=_EMPIRIOLABS_RESPONSE)
response: Final = litellm.systemone(
model=_EMPIRIOLABS_MODEL,
state=_EMPIRIOLABS_STATE,
questions=_EMPIRIOLABS_QUESTIONS,
api_base=api_base,
api_key="caller-key",
)
assert route.call_count == 1
assert json.loads(route.calls[0].request.content) == {
"model": "aplomb-1",
"state": _EMPIRIOLABS_STATE,
"questions": {"is_refund": {"type": "noul", "instructions": "Is the customer asking for a refund?"}},
}
assert response.answers["is_refund"] == NoulAnswer(type="noul", noul=0.97, confidence=0.94)
assert response._hidden_params["custom_llm_provider"] == "empiriolabs"
@pytest.mark.asyncio
@pytest.mark.parametrize(
("api_key", "authorization"),
((None, "Bearer environment-key"), ("caller-key", "Bearer caller-key")),
ids=("environment_key", "caller_key_over_environment"),
)
async def test_empiriolabs_sends_the_caller_key_or_else_the_environment_key(
api_key: str | None,
authorization: str,
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.delenv("EMPIRIOLABS_API_BASE", raising=False)
monkeypatch.setenv("EMPIRIOLABS_API_KEY", "environment-key")
route: Final = respx_mock.post(_EMPIRIOLABS_DEFAULT_URL).respond(json=_EMPIRIOLABS_RESPONSE)
response: Final = await litellm.adecisions(
model=_EMPIRIOLABS_MODEL,
input=_EMPIRIOLABS_STATE,
questions=[{"type": "predicate", "name": "is_refund", "instructions": "Is the customer asking for a refund?"}],
api_key=api_key,
)
assert route.call_count == 1
assert route.calls[0].request.headers["authorization"] == authorization
assert isinstance(response, OpenAIDecisionResponse)
assert [answer.model_dump(mode="json") for answer in response.answers] == [
{"type": "predicate", "name": "is_refund", "probability": 0.97}
]
def test_empiriolabs_without_a_key_is_refused_before_http(
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.delenv("EMPIRIOLABS_API_KEY", raising=False)
monkeypatch.delenv("EMPIRIOLABS_API_BASE", raising=False)
with pytest.raises(litellm.AuthenticationError, match="Missing API key for Decisions provider 'empiriolabs'"):
litellm.systemone(model=_EMPIRIOLABS_MODEL, state=_EMPIRIOLABS_STATE, questions=_EMPIRIOLABS_QUESTIONS)
assert len(respx_mock.calls) == 0
@pytest.mark.parametrize(
("status", "error_type", "code", "exception"),
(
(401, "authentication_error", "invalid_api_key", litellm.AuthenticationError),
(402, "insufficient_credits", "insufficient_credits", litellm.PaymentRequiredError),
(404, "not_found_error", "model_not_found", litellm.NotFoundError),
(422, "invalid_request_error", "invalid_request", litellm.BadRequestError),
(429, "rate_limit_error", "rate_limit_exceeded", litellm.RateLimitError),
(503, "service_unavailable", "model_unavailable", litellm.ServiceUnavailableError),
),
)
def test_empiriolabs_upstream_errors_keep_their_status_and_message(
status: int,
error_type: str,
code: str,
exception: type[Exception],
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.delenv("EMPIRIOLABS_API_BASE", raising=False)
message: Final = f"upstream refused the decision with {code}"
route: Final = respx_mock.post(_EMPIRIOLABS_DEFAULT_URL).respond(
status,
json={"error": {"message": message, "type": error_type, "code": code}},
headers={"retry-after": "1"},
)
with pytest.raises(exception, match=message) as caught:
litellm.systemone(
model=_EMPIRIOLABS_MODEL,
state=_EMPIRIOLABS_STATE,
questions=_EMPIRIOLABS_QUESTIONS,
api_key="caller-key",
num_retries=0,
)
assert getattr(caught.value, "status_code") == status
assert route.call_count == 1
_BOX_QUESTION_REQUESTS: Final[tuple[tuple[str, Mapping[str, object]], ...]] = (
(
"decisions",

View file

View file

@ -0,0 +1,40 @@
from collections.abc import Mapping
import pytest
from litellm.llms.empiriolabs.decisions.transformation import EmpirioLabsDecisionsConfig
@pytest.mark.parametrize(
("environment", "connection"),
(
({}, ("https://api.empiriolabs.ai", None)),
(
{"EMPIRIOLABS_API_BASE": "https://gateway.example/v1", "EMPIRIOLABS_API_KEY": "environment-key"},
("https://gateway.example/v1", "environment-key"),
),
),
ids=("public_api_without_a_key", "environment"),
)
def test_connection_comes_from_the_environment_before_the_public_api(
environment: Mapping[str, str],
connection: tuple[str, str | None],
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("EMPIRIOLABS_API_BASE", raising=False)
monkeypatch.delenv("EMPIRIOLABS_API_KEY", raising=False)
for name, value in environment.items():
monkeypatch.setenv(name, value)
assert EmpirioLabsDecisionsConfig().resolve_connection(None, None) == connection
@pytest.mark.parametrize(
"api_base",
("https://api.empiriolabs.ai", "https://api.empiriolabs.ai/v1", "https://api.empiriolabs.ai/v1/"),
)
def test_requests_go_to_v1_decisions_whether_or_not_the_base_ends_in_v1(api_base: str) -> None:
assert (
EmpirioLabsDecisionsConfig().get_complete_url(api_base=api_base, model="aplomb-1")
== "https://api.empiriolabs.ai/v1/decisions"
)

View file

@ -2,13 +2,19 @@
Unit tests for the EmpirioLabs OpenAI-like provider.
"""
import json
import os
import sys
from typing import Final
import pytest
import respx
sys.path.insert(
0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../.."))
)
import litellm
from litellm.llms.openai_like.dynamic_config import create_config_class
from litellm.llms.openai_like.json_loader import JSONProviderRegistry
@ -61,3 +67,50 @@ def test_empiriolabs_complete_url_appends_endpoint():
stream=False,
)
assert url == f"{EMPIRIOLABS_BASE_URL}/chat/completions"
def test_empiriolabs_chat_models_still_route_through_the_json_provider(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("EMPIRIOLABS_API_KEY", "environment-key")
monkeypatch.delenv("EMPIRIOLABS_API_BASE", raising=False)
with respx.mock() as upstream:
route: Final = upstream.post(f"{EMPIRIOLABS_BASE_URL}/chat/completions").respond(
200,
json={
"id": "chatcmpl_empiriolabs",
"object": "chat.completion",
"created": 1_789_550_000,
"model": "qwen3-7-plus",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Hello from EmpirioLabs AI"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 4, "completion_tokens": 5, "total_tokens": 9},
},
)
response: Final = litellm.completion(
model="empiriolabs/qwen3-7-plus",
messages=[{"role": "user", "content": "Say hello"}],
max_completion_tokens=16,
)
request: Final = route.calls.last.request
assert litellm.get_llm_provider("empiriolabs/qwen3-7-plus") == (
"qwen3-7-plus",
"empiriolabs",
"environment-key",
EMPIRIOLABS_BASE_URL,
)
assert route.call_count == 1
assert request.headers["authorization"] == "Bearer environment-key"
assert json.loads(request.content) == {
"model": "qwen3-7-plus",
"messages": [{"role": "user", "content": "Say hello"}],
"max_tokens": 16,
}
assert response.choices[0].message.content == "Hello from EmpirioLabs AI"

View file

@ -438,9 +438,16 @@ def _decisions_provider_entry(provider: str) -> ProviderCreateInfo:
return entry
@pytest.mark.parametrize("display_name,provider", (("TypeSafe", LlmProviders.TYPESAFE), ("Nace", LlmProviders.NACE)))
def test_decisions_provider_fields(display_name: str, provider: LlmProviders) -> None:
entry: Final = _decisions_provider_entry(display_name)
@pytest.mark.parametrize(
("key", "display_name", "provider"),
(
("TypeSafe", "TypeSafe", LlmProviders.TYPESAFE),
("Nace", "Nace", LlmProviders.NACE),
("EMPIRIOLABS", "EmpirioLabs AI", LlmProviders.EMPIRIOLABS),
),
)
def test_decisions_provider_fields(key: str, display_name: str, provider: LlmProviders) -> None:
entry: Final = _decisions_provider_entry(key)
assert entry.provider_display_name == display_name
assert entry.litellm_provider == provider.value

View file

@ -206,6 +206,7 @@ describe("provider_info_helpers", () => {
Providers.DATAROBOT,
Providers.DOCKER_MODEL_RUNNER,
Providers.DOTPROMPT,
Providers.EMPIRIOLABS,
Providers.EMPOWER,
Providers.GALADRIEL,
Providers.GradientAI,
@ -541,6 +542,24 @@ describe("provider_info_helpers", () => {
]);
});
it("should list EmpirioLabs AI chat and decision models for the 'EMPIRIOLABS' provider key", () => {
const modelMap = {
"empiriolabs/aplomb-1": {
litellm_provider: "empiriolabs",
mode: "chat",
supported_endpoints: ["/v1/chat/completions", "/v1/decisions"],
},
"empiriolabs/qwen3-7-plus": { litellm_provider: "empiriolabs", mode: "chat" },
"typesafe/jev-latest": { litellm_provider: "typesafe", mode: "evaluation" },
};
expect(getProviderModels("EMPIRIOLABS" as Providers, modelMap)).toEqual([
"empiriolabs/aplomb-1",
"empiriolabs/qwen3-7-plus",
]);
expect(getProviderLogoAndName("empiriolabs").displayName).toBe("EmpirioLabs AI");
expect(getPlaceholder("EMPIRIOLABS")).toBe("empiriolabs/aplomb-1");
});
it("should list only strands_decider models for the 'StrandsDecider' provider key", () => {
const modelMap = {
"strands_decider/strands-decider-2B-hobson-v19": { litellm_provider: "strands_decider", mode: "decisions" },

View file

@ -113,6 +113,7 @@ export enum Providers {
DOTPROMPT = "Dotprompt",
EDENAI = "Eden AI",
ElevenLabs = "ElevenLabs",
EMPIRIOLABS = "EmpirioLabs AI",
EMPOWER = "Empower",
FalAI = "Fal AI",
FEATHERLESS_AI = "Featherless Ai",
@ -240,6 +241,7 @@ export const provider_map: Record<string, string> = {
DOTPROMPT: "dotprompt",
EDENAI: "edenai",
ElevenLabs: "elevenlabs",
EMPIRIOLABS: "empiriolabs",
EMPOWER: "empower",
FalAI: "fal_ai",
FEATHERLESS_AI: "featherless_ai",
@ -475,6 +477,7 @@ const providerPlaceholderMap: Partial<Record<Providers, string>> = {
[Providers.Cursor]: "cursor/claude-4-sonnet",
[Providers.DeepInfra]: "deepinfra/<any-model-on-deepinfra>",
[Providers.EDENAI]: "edenai/openai/gpt-mini-latest",
[Providers.EMPIRIOLABS]: "empiriolabs/aplomb-1",
[Providers.FalAI]: "fal_ai/fal-ai/flux-pro/v1.1-ultra",
[Providers.Google_AI_Studio]: "gemini-pro",
[Providers.JinaAI]: "jina_ai/",