From 7baadbbb34004e7c3425e8101758c9d25771446e Mon Sep 17 00:00:00 2001 From: Florian Standhartinger <1905188+fstandhartinger@users.noreply.github.com> Date: Sun, 25 Oct 2026 21:34:12 +0000 Subject: [PATCH] feat: add Decision Models decisions provider Signed-off-by: Florian Standhartinger --- README.md | 1 + litellm/__init__.py | 3 + litellm/_lazy_imports_registry.py | 2 + .../decisions/transformation.py | 9 ++ ...odel_prices_and_context_window_backup.json | 32 +++++++ .../provider_create_fields.json | 28 +++++++ litellm/types/utils.py | 1 + litellm/utils.py | 2 + model_prices_and_context_window.json | 32 +++++++ provider_endpoints_support.json | 7 ++ .../providers/test_decisions_wire.py | 3 + tests/unit/decisions/test_main.py | 83 +++++++++++++++++++ .../public_endpoints/test_public_endpoints.py | 1 + .../components/provider_info_helpers.test.tsx | 11 +++ .../src/components/provider_info_helpers.tsx | 3 + 15 files changed, 218 insertions(+) create mode 100644 litellm/llms/decisionmodels/decisions/transformation.py diff --git a/README.md b/README.md index 31a93bf8c67..64db000e7b7 100644 --- a/README.md +++ b/README.md @@ -393,6 +393,7 @@ Set `LITELLM_PROXY_API_BASE` and `LITELLM_PROXY_API_KEY` and every model call th | [Dashscope (`dashscope`)](https://docs.litellm.ai/docs/providers/dashscope) | ✅ | ✅ | ✅ | ✅ | | | | | | ✅ | | [Databricks (`databricks`)](https://docs.litellm.ai/docs/providers/databricks) | ✅ | ✅ | ✅ | | | | | | | | | [DataRobot (`datarobot`)](https://docs.litellm.ai/docs/providers/datarobot) | ✅ | ✅ | ✅ | | | | | | | | +| [Decision Models (`decisionmodels`)](https://decisionmodels.io) | | | | | | | | | | | | [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) | ✅ | ✅ | ✅ | | | | | | | | diff --git a/litellm/__init__.py b/litellm/__init__.py index 8e5dd68a186..218fee17a31 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -1763,6 +1763,9 @@ if TYPE_CHECKING: from .llms.parallel_ai.decisions.transformation import ( ParallelAIDecisionsConfig as ParallelAIDecisionsConfig, ) + from .llms.decisionmodels.decisions.transformation import ( + DecisionModelsDecisionsConfig as DecisionModelsDecisionsConfig, + ) from .llms.openrouter.decisions.transformation import ( OpenRouterDecisionsConfig as OpenRouterDecisionsConfig, ) diff --git a/litellm/_lazy_imports_registry.py b/litellm/_lazy_imports_registry.py index bf903e23bc0..77c6fe69975 100644 --- a/litellm/_lazy_imports_registry.py +++ b/litellm/_lazy_imports_registry.py @@ -165,6 +165,7 @@ LLM_CONFIG_NAMES: Final = ( "TypeSafeDecisionsConfig", "ParallelAIDecisionsConfig", "NaceDecisionsConfig", + "DecisionModelsDecisionsConfig", "OpenRouterDecisionsConfig", "FireworksAIDecisionsConfig", "CloudflareDecisionsConfig", @@ -743,6 +744,7 @@ _LLM_CONFIGS_IMPORT_MAP: Final = { "NaceDecisionsConfig": (".llms.nace.decisions.transformation", "NaceDecisionsConfig"), "TypeSafeDecisionsConfig": (".llms.typesafe.decisions.transformation", "TypeSafeDecisionsConfig"), "ParallelAIDecisionsConfig": (".llms.parallel_ai.decisions.transformation", "ParallelAIDecisionsConfig"), + "DecisionModelsDecisionsConfig": (".llms.decisionmodels.decisions.transformation", "DecisionModelsDecisionsConfig"), "OpenRouterDecisionsConfig": (".llms.openrouter.decisions.transformation", "OpenRouterDecisionsConfig"), "FireworksAIDecisionsConfig": (".llms.fireworks_ai.decisions.transformation", "FireworksAIDecisionsConfig"), "CloudflareDecisionsConfig": (".llms.cloudflare.decisions.transformation", "CloudflareDecisionsConfig"), diff --git a/litellm/llms/decisionmodels/decisions/transformation.py b/litellm/llms/decisionmodels/decisions/transformation.py new file mode 100644 index 00000000000..6a34d195016 --- /dev/null +++ b/litellm/llms/decisionmodels/decisions/transformation.py @@ -0,0 +1,9 @@ +from litellm.llms.base_llm.decisions.transformation import BaseDecisionsConfig + + +class DecisionModelsDecisionsConfig(BaseDecisionsConfig): + api_key_env = ("DECISIONMODELS_API_KEY",) + api_base_env = ("DECISIONMODELS_API_BASE",) + + def get_default_api_base(self) -> str | None: + return "https://api.decisionmodels.io" diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 76fd85d001b..bc4c61f3b32 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -74067,6 +74067,38 @@ "output_cost_per_token": 0.0, "source": "https://console.nace.ai/docs/drex/reference/models" }, + "decisionmodels/s1-fast": { + "input_cost_per_token": 1.9e-08, + "litellm_provider": "decisionmodels", + "max_input_tokens": 16192, + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://api.decisionmodels.io/v1/models" + }, + "decisionmodels/s1-pro": { + "input_cost_per_token": 8e-08, + "litellm_provider": "decisionmodels", + "max_input_tokens": 8192, + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://api.decisionmodels.io/v1/models" + }, + "decisionmodels/reax-1": { + "input_cost_per_token": 8e-08, + "litellm_provider": "decisionmodels", + "max_input_tokens": 8192, + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://api.decisionmodels.io/v1/models" + }, + "decisionmodels/s1-llm-auto-router": { + "input_cost_per_token": 1e-09, + "litellm_provider": "decisionmodels", + "max_input_tokens": 8192, + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://api.decisionmodels.io/v1/models" + }, "typesafe/jev-1.13.0": { "input_cost_per_token": 4.2e-08, "litellm_provider": "typesafe", diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 5ded2fa5f5e..eac81a7cf40 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -1397,6 +1397,34 @@ ], "default_model_placeholder": "gpt-3.5-turbo" }, + { + "provider": "DecisionModels", + "provider_display_name": "Decision Models", + "litellm_provider": "decisionmodels", + "credential_fields": [ + { + "key": "api_base", + "label": "API Base", + "placeholder": "https://api.decisionmodels.io", + "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": "decisionmodels/s1-fast" + }, { "provider": "Deepgram", "provider_display_name": "Deepgram", diff --git a/litellm/types/utils.py b/litellm/types/utils.py index ba29a67a36c..a80963ed6c7 100644 --- a/litellm/types/utils.py +++ b/litellm/types/utils.py @@ -4648,6 +4648,7 @@ class LlmProviders(str, Enum): TYPESAFE = "typesafe" PARALLEL_AI = "parallel_ai" NACE = "nace" + DECISIONMODELS = "decisionmodels" STRANDS_DECIDER = "strands_decider" LAYA = "laya" BESPOKE = "bespoke" diff --git a/litellm/utils.py b/litellm/utils.py index 53b02ebddac..a82077eead9 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -10320,6 +10320,7 @@ class ProviderConfigManager: LlmProviders.AZURE_AI, LlmProviders.EU_JEV, LlmProviders.PARALLEL_AI, + LlmProviders.DECISIONMODELS, ): configs: Final = { LlmProviders.EMPIRIOLABS: litellm.EmpirioLabsDecisionsConfig, @@ -10328,6 +10329,7 @@ class ProviderConfigManager: LlmProviders.AZURE_AI: litellm.AzureAIDecisionsConfig, LlmProviders.EU_JEV: litellm.EuJevDecisionsConfig, LlmProviders.PARALLEL_AI: litellm.ParallelAIDecisionsConfig, + LlmProviders.DECISIONMODELS: litellm.DecisionModelsDecisionsConfig, } return configs[provider]() return None diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 76fd85d001b..bc4c61f3b32 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -74067,6 +74067,38 @@ "output_cost_per_token": 0.0, "source": "https://console.nace.ai/docs/drex/reference/models" }, + "decisionmodels/s1-fast": { + "input_cost_per_token": 1.9e-08, + "litellm_provider": "decisionmodels", + "max_input_tokens": 16192, + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://api.decisionmodels.io/v1/models" + }, + "decisionmodels/s1-pro": { + "input_cost_per_token": 8e-08, + "litellm_provider": "decisionmodels", + "max_input_tokens": 8192, + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://api.decisionmodels.io/v1/models" + }, + "decisionmodels/reax-1": { + "input_cost_per_token": 8e-08, + "litellm_provider": "decisionmodels", + "max_input_tokens": 8192, + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://api.decisionmodels.io/v1/models" + }, + "decisionmodels/s1-llm-auto-router": { + "input_cost_per_token": 1e-09, + "litellm_provider": "decisionmodels", + "max_input_tokens": 8192, + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://api.decisionmodels.io/v1/models" + }, "typesafe/jev-1.13.0": { "input_cost_per_token": 4.2e-08, "litellm_provider": "typesafe", diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 00fee0a0d5a..140a9342563 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -4046,6 +4046,13 @@ "generateContent": false } }, + "decisionmodels": { + "display_name": "Decision Models (`decisionmodels`)", + "url": "https://decisionmodels.io", + "endpoints": { + "systemone": true + } + }, "typesafe": { "display_name": "TypeSafe (`typesafe`)", "url": "https://docs.typesafe.ai/models", diff --git a/tests/integration/providers/test_decisions_wire.py b/tests/integration/providers/test_decisions_wire.py index bb4973d6f2f..6637e4846ec 100644 --- a/tests/integration/providers/test_decisions_wire.py +++ b/tests/integration/providers/test_decisions_wire.py @@ -91,6 +91,9 @@ _PROVIDERS: Final = ( ), _Provider("typesafe", "typesafe/jev-1.13.0", "/v1/systemone", "jev-1.13.0", _API_KEY, False, "typesafe/jev-1.13.0"), _Provider("nace", "nace/drex-v2.0", "/v1/systemone", "drex-v2.0", _API_KEY, False, "nace/drex-v2.0"), + _Provider( + "decisionmodels", "decisionmodels/s1-fast", "/v1/systemone", "s1-fast", _API_KEY, False, "decisionmodels/s1-fast" + ), _Provider( "openrouter", "openrouter/typesafe/jev-1.13", diff --git a/tests/unit/decisions/test_main.py b/tests/unit/decisions/test_main.py index 0e2813e0a0d..b4b95b8e2d3 100644 --- a/tests/unit/decisions/test_main.py +++ b/tests/unit/decisions/test_main.py @@ -128,6 +128,7 @@ _PROVIDERS: Final[tuple[tuple[str, str, str, str], ...]] = ( ("typesafe", "typesafe/jev-1.13", "https://api.typesafe.ai/v1/systemone", "jev-1.13"), ("nace", "nace/drex-v2.0", "https://console.nace.ai/v1/systemone", "drex-v2.0"), ("eu_jev", "eu_jev/eu-jeff-latest", "https://eu-jev.com/v1/systemone", "eu-jeff-latest"), + ("decisionmodels", "decisionmodels/s1-fast", "https://api.decisionmodels.io/v1/systemone", "s1-fast"), ( "openrouter", "openrouter/typesafe/jev-1.13", @@ -4905,3 +4906,85 @@ def test_hosted_vllm_systemone_maps_image_rejection_to_bad_request( assert caught.value.status_code == 400 assert route.call_count == 1 + + +@pytest.mark.parametrize("model", ("s1-fast", "s1-pro", "reax-1", "s1-llm-auto-router")) +def test_decisionmodels_prices_are_input_only(model: str) -> None: + entry: Final = litellm.model_cost[f"decisionmodels/{model}"] + + assert entry["litellm_provider"] == "decisionmodels" + assert entry["mode"] == "evaluation" + assert entry["max_input_tokens"] > 0 + assert entry["input_cost_per_token"] > 0 + assert entry["output_cost_per_token"] == 0 + + +def test_decisionmodels_sync_call_uses_default_base_and_environment_key( + monkeypatch: pytest.MonkeyPatch, + respx_mock: respx.MockRouter, +) -> None: + monkeypatch.setenv("DECISIONMODELS_API_KEY", "environment-key") + monkeypatch.delenv("DECISIONMODELS_API_BASE", raising=False) + upstream: Final = respx_mock.post("https://api.decisionmodels.io/v1/systemone").respond(json=_RESPONSE) + response: Final = litellm.systemone( + model="decisionmodels/s1-fast", + state="damaged order", + questions={"is_defect": {"type": "noul", "instructions": "Is this a defect?"}}, + ) + assert response.answers["is_defect"] == NoulAnswer(type="noul", noul=0.9) + assert upstream.calls[0].request.headers["authorization"] == "Bearer environment-key" + assert json.loads(upstream.calls[0].request.content) == { + "model": "s1-fast", + "state": "damaged order", + "questions": {"is_defect": {"type": "noul", "instructions": "Is this a defect?"}}, + } + + +def test_decisionmodels_environment_base_and_explicit_key_win( + monkeypatch: pytest.MonkeyPatch, + respx_mock: respx.MockRouter, +) -> None: + monkeypatch.setenv("DECISIONMODELS_API_BASE", "https://decisionmodels.example/") + monkeypatch.setenv("DECISIONMODELS_API_KEY", "environment-key") + upstream: Final = respx_mock.post("https://decisionmodels.example/v1/systemone").respond(json=_RESPONSE) + litellm.systemone( + model="decisionmodels/s1-pro", + state={"message": "damaged order"}, + api_key="explicit-key", + questions={"is_defect": {"type": "noul", "criteria": {"true": "damaged", "false": "intact"}}}, + ) + assert upstream.calls[0].request.headers["authorization"] == "Bearer explicit-key" + assert json.loads(upstream.calls[0].request.content)["model"] == "s1-pro" + + +@pytest.mark.parametrize( + ("status", "exception"), + ( + (401, litellm.AuthenticationError), + (402, litellm.PaymentRequiredError), + (422, litellm.BadRequestError), + (429, litellm.RateLimitError), + ), +) +def test_decisionmodels_upstream_failures_preserve_status_and_message( + status: int, + exception: type[Exception], + monkeypatch: pytest.MonkeyPatch, + respx_mock: respx.MockRouter, +) -> None: + monkeypatch.delenv("DECISIONMODELS_API_BASE", raising=False) + upstream: Final = respx_mock.post("https://api.decisionmodels.io/v1/systemone").respond( + status, + json={"detail": {"error_type": "test_error", "message": "decision rejected"}}, + headers={"retry-after": "1"}, + ) + with pytest.raises(exception, match="decision rejected") as error: + litellm.systemone( + model="decisionmodels/s1-fast", + state="text", + api_key="test-key", + questions={"a": {"type": "noul", "instructions": "Is it urgent?"}}, + num_retries=0, + ) + assert error.value.status_code == status + assert upstream.call_count == 1 diff --git a/tests/unit/proxy/public_endpoints/test_public_endpoints.py b/tests/unit/proxy/public_endpoints/test_public_endpoints.py index 6591f7184d3..197bb359a4a 100644 --- a/tests/unit/proxy/public_endpoints/test_public_endpoints.py +++ b/tests/unit/proxy/public_endpoints/test_public_endpoints.py @@ -530,6 +530,7 @@ def _decisions_provider_entry(provider: str) -> ProviderCreateInfo: ("PARALLEL_AI", "Parallel AI", LlmProviders.PARALLEL_AI), ("EU_JEV", "eu/jev", LlmProviders.EU_JEV), ("EMPIRIOLABS", "EmpirioLabs AI", LlmProviders.EMPIRIOLABS), + ("DecisionModels", "Decision Models", LlmProviders.DECISIONMODELS), ), ) def test_decisions_provider_fields(provider_key: str, display_name: str, provider: LlmProviders) -> None: diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx index 604d5923caa..65e3e86c85f 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx @@ -250,6 +250,7 @@ describe("provider_info_helpers", () => { Providers.COMPACTIFAI, Providers.CORALBRICKS, Providers.DATAROBOT, + Providers.DecisionModels, Providers.DOCKER_MODEL_RUNNER, Providers.DOTPROMPT, Providers.EMPIRIOLABS, @@ -604,6 +605,16 @@ describe("provider_info_helpers", () => { expect(getPlaceholder("Nace")).toBe("nace/drex-latest"); }); + it("lists Decision Models decision models and resolves the provider name and model placeholder", () => { + const modelMap = { + "decisionmodels/s1-fast": { litellm_provider: "decisionmodels", mode: "evaluation" }, + "typesafe/jev-latest": { litellm_provider: "typesafe", mode: "evaluation" }, + }; + expect(getProviderModels("DecisionModels" as Providers, modelMap)).toEqual(["decisionmodels/s1-fast"]); + expect(getProviderLogoAndName("decisionmodels").displayName).toBe("Decision Models"); + expect(getPlaceholder("DecisionModels")).toBe("decisionmodels/s1-fast"); + }); + it("should list only typesafe decision models for the 'TypeSafe' provider key, not the OpenRouter-hosted one", () => { const modelMap = { "typesafe/jev-latest": { litellm_provider: "typesafe", mode: "evaluation" }, diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx index 167343fc05e..07649a50f48 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx @@ -111,6 +111,7 @@ export enum Providers { Dashscope = "Dashscope", Databricks = "Databricks (Qwen API)", DATAROBOT = "Datarobot", + DecisionModels = "Decision Models", DeepInfra = "DeepInfra", Deepgram = "Deepgram", Deepseek = "Deepseek", @@ -246,6 +247,7 @@ export const provider_map: Record = { Dashscope: "dashscope", Databricks: "databricks", DATAROBOT: "datarobot", + DecisionModels: "decisionmodels", DeepInfra: "deepinfra", Deepgram: "deepgram", Deepseek: "deepseek", @@ -499,6 +501,7 @@ const providerPlaceholderMap: Partial> = { [Providers.Cognition]: "cognition/swe-1.7", [Providers.CORALBRICKS]: "coralbricks/glm-5.3-fast", [Providers.Cursor]: "cursor/claude-4-sonnet", + [Providers.DecisionModels]: "decisionmodels/s1-fast", [Providers.DeepInfra]: "deepinfra/", [Providers.EDENAI]: "edenai/openai/gpt-mini-latest", [Providers.EMPIRIOLABS]: "empiriolabs/aplomb-1",