From 3a3b8cad809f1a2d530d7572cd770dc35b796d30 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 04:04:07 -0700 Subject: [PATCH] feat(ui): add TypeSafe and Strands Decider to the Add Model provider list (#45335) * feat(ui): add TypeSafe and Strands Decider to the Add Model provider list * test(public_endpoints): type the provider fields helper against the route's model --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com> --- .../provider_create_fields.json | 56 ++++++++++++++++++ .../public_endpoints/test_public_endpoints.py | 51 +++++++++++++++- .../public/assets/logos/typesafe.png | Bin 0 -> 4311 bytes .../components/provider_info_helpers.test.tsx | 49 +++++++++++++++ .../src/components/provider_info_helpers.tsx | 10 ++++ 5 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 ui/litellm-dashboard/public/assets/logos/typesafe.png diff --git a/litellm/proxy/public_endpoints/provider_create_fields.json b/litellm/proxy/public_endpoints/provider_create_fields.json index 2dd9f50bec8..ae07d5d6bd9 100644 --- a/litellm/proxy/public_endpoints/provider_create_fields.json +++ b/litellm/proxy/public_endpoints/provider_create_fields.json @@ -3189,6 +3189,34 @@ ], "default_model_placeholder": "soniox/stt-async-v5" }, + { + "provider": "StrandsDecider", + "provider_display_name": "Strands Decider", + "litellm_provider": "strands_decider", + "credential_fields": [ + { + "key": "api_base", + "label": "API Base", + "placeholder": "http://localhost:8000", + "tooltip": "URL of your self-hosted Strands Decider server", + "required": true, + "field_type": "text", + "options": null, + "default_value": null + }, + { + "key": "api_key", + "label": "API Key", + "placeholder": null, + "tooltip": null, + "required": false, + "field_type": "password", + "options": null, + "default_value": null + } + ], + "default_model_placeholder": "strands_decider/strands-decider-2B-hobson-v19" + }, { "provider": "Tencent", "provider_display_name": "Tencent", @@ -3319,6 +3347,34 @@ ], "default_model_placeholder": "gpt-3.5-turbo" }, + { + "provider": "TypeSafe", + "provider_display_name": "TypeSafe", + "litellm_provider": "typesafe", + "credential_fields": [ + { + "key": "api_base", + "label": "API Base", + "placeholder": "https://api.typesafe.ai", + "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": "typesafe/jev-latest" + }, { "provider": "V0", "provider_display_name": "V0", diff --git a/tests/unit/proxy/public_endpoints/test_public_endpoints.py b/tests/unit/proxy/public_endpoints/test_public_endpoints.py index 30538c1167f..7e20b5f8c3c 100644 --- a/tests/unit/proxy/public_endpoints/test_public_endpoints.py +++ b/tests/unit/proxy/public_endpoints/test_public_endpoints.py @@ -9,6 +9,7 @@ import pytest from fastapi import FastAPI from fastapi.testclient import TestClient +from pydantic import TypeAdapter from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.public_endpoints import router @@ -16,6 +17,7 @@ from litellm.router_strategy.complexity_router.fuse_presets import get_fuse_pres from litellm.types.proxy.management_endpoints.model_management_endpoints import ( ModelGroupInfoProxy, ) +from litellm.types.proxy.public_endpoints.public_endpoints import ProviderCreateInfo from litellm.types.utils import LlmProviders @@ -402,6 +404,53 @@ def test_tencent_provider_fields(): assert fields_by_key["api_base"]["required"] is False +def _decisions_provider_entry(provider: str) -> ProviderCreateInfo: + app_instance: Final = FastAPI() + app_instance.include_router(router) + test_client: Final = TestClient(app_instance) + + response: Final = test_client.get("/public/providers/fields") + assert response.status_code == 200 + providers: Final = TypeAdapter(list[ProviderCreateInfo]).validate_python(response.json()) + entry: Final = next((p for p in providers if p.provider == provider), None) + assert entry is not None, f"{provider} provider entry not found" + return entry + + +def test_typesafe_provider_fields(): + typesafe: Final = _decisions_provider_entry("TypeSafe") + + assert typesafe.provider_display_name == "TypeSafe" + assert typesafe.litellm_provider == LlmProviders.TYPESAFE.value + assert typesafe.default_model_placeholder is not None + assert typesafe.default_model_placeholder.startswith("typesafe/") + + fields_by_key: Final = {f.key: f for f in typesafe.credential_fields} + + assert fields_by_key["api_key"].required is True + assert fields_by_key["api_key"].field_type == "password" + + assert fields_by_key["api_base"].required is False + assert fields_by_key["api_base"].field_type == "text" + + +def test_strands_decider_provider_fields(): + strands: Final = _decisions_provider_entry("StrandsDecider") + + assert strands.provider_display_name == "Strands Decider" + assert strands.litellm_provider == LlmProviders.STRANDS_DECIDER.value + assert strands.default_model_placeholder is not None + assert strands.default_model_placeholder.startswith("strands_decider/") + + fields_by_key: Final = {f.key: f for f in strands.credential_fields} + + assert fields_by_key["api_base"].required is True + assert fields_by_key["api_base"].field_type == "text" + + assert fields_by_key["api_key"].required is False + assert fields_by_key["api_key"].field_type == "password" + + ADD_MODEL_UNLISTED_PROVIDERS: Final = frozenset( { "a2a", @@ -436,12 +485,10 @@ ADD_MODEL_UNLISTED_PROVIDERS: Final = frozenset( "sagemaker_nova", "scaleway", "stability", - "strands_decider", "synthetic", "tensormesh", "text-completion-inception", "transcribe", - "typesafe", "valkey", "xiaomi_mimo", "zai", diff --git a/ui/litellm-dashboard/public/assets/logos/typesafe.png b/ui/litellm-dashboard/public/assets/logos/typesafe.png new file mode 100644 index 0000000000000000000000000000000000000000..45107c7d09d7e53da800df531d92a5eaafd08f99 GIT binary patch literal 4311 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 ABLDyZ literal 0 HcmV?d00001 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 7cfdaf3275d..cecd6ce01bf 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx @@ -214,6 +214,26 @@ describe("provider_info_helpers", () => { const { logo } = getProviderLogoAndName("tencent"); expect(logo).toContain("tencent"); }); + + it("should resolve the typesafe slug and TypeSafe enum key to the TypeSafe name and bundled logo", () => { + const fromSlug = getProviderLogoAndName("typesafe"); + expect(fromSlug.displayName).toBe(Providers.TypeSafe); + expect(fromSlug.logo).toContain("typesafe"); + + const fromEnumKey = getProviderLogoAndName("TypeSafe"); + expect(fromEnumKey.displayName).toBe(Providers.TypeSafe); + expect(fromEnumKey.logo).toBe(fromSlug.logo); + }); + + it("should resolve the strands_decider slug and StrandsDecider enum key to the Strands Decider name and bundled logo", () => { + const fromSlug = getProviderLogoAndName("strands_decider"); + expect(fromSlug.displayName).toBe("Strands Decider"); + expect(fromSlug.logo).toContain("strands"); + + const fromEnumKey = getProviderLogoAndName("StrandsDecider"); + expect(fromEnumKey.displayName).toBe(Providers.StrandsDecider); + expect(fromEnumKey.logo).toBe(fromSlug.logo); + }); }); describe("getPlaceholder", () => { @@ -318,6 +338,11 @@ describe("provider_info_helpers", () => { expect(getPlaceholder(Providers.Tencent)).toBe("tencent/deepseek-v4-pro"); }); + it("should return decision model placeholders for the TypeSafe and StrandsDecider dropdown keys", () => { + expect(getPlaceholder("TypeSafe")).toBe("typesafe/jev-latest"); + expect(getPlaceholder("StrandsDecider")).toBe("strands_decider/strands-decider-2B-hobson-v19"); + }); + it("should return default gpt-3.5-turbo placeholder for unknown provider", () => { expect(getPlaceholder("UnknownProvider" as any)).toBe("gpt-3.5-turbo"); }); @@ -421,6 +446,30 @@ describe("provider_info_helpers", () => { expect(getProviderModels("Sail" as Providers, modelMap)).toEqual(["sail/openai/gpt-oss-120b"]); }); + 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" }, + "typesafe/jev-preview": { litellm_provider: "typesafe", mode: "evaluation" }, + "openrouter/typesafe/jev-1.13": { litellm_provider: "openrouter", mode: "evaluation" }, + "strands_decider/strands-decider-2B-hobson-v19": { litellm_provider: "strands_decider", mode: "evaluation" }, + }; + expect(getProviderModels("TypeSafe" as Providers, modelMap)).toEqual([ + "typesafe/jev-latest", + "typesafe/jev-preview", + ]); + }); + + 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: "evaluation" }, + "typesafe/jev-latest": { litellm_provider: "typesafe", mode: "evaluation" }, + "openrouter/typesafe/jev-1.13": { litellm_provider: "openrouter", mode: "evaluation" }, + }; + expect(getProviderModels("StrandsDecider" as Providers, modelMap)).toEqual([ + "strands_decider/strands-decider-2B-hobson-v19", + ]); + }); + it("should include bedrock converse but exclude standalone bedrock_mantle when called with 'Bedrock' provider key", () => { const modelMap = { "bedrock-base": { litellm_provider: "bedrock" }, diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx index 5ea693bea10..5a57fc23dde 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx @@ -55,9 +55,11 @@ import sapLogo from "../../public/assets/logos/sap.png"; import scxAiLogo from "../../public/assets/logos/scx_ai.svg"; import snowflakeLogo from "../../public/assets/logos/snowflake.svg"; import sonioxLogo from "../../public/assets/logos/soniox.svg"; +import strandsLogo from "../../public/assets/logos/strands.svg"; import tencentLogo from "../../public/assets/logos/tencent.svg"; import togetheraiLogo from "../../public/assets/logos/togetherai.svg"; import topazLogo from "../../public/assets/logos/topaz.svg"; +import typesafeLogo from "../../public/assets/logos/typesafe.png"; import v0Logo from "../../public/assets/logos/v0.svg"; import vercelLogo from "../../public/assets/logos/vercel.svg"; import vllmLogo from "../../public/assets/logos/vllm.png"; @@ -167,11 +169,13 @@ export enum Providers { SCX_AI = "SCX.ai", Snowflake = "Snowflake", Soniox = "Soniox", + StrandsDecider = "Strands Decider", TEXT_COMPLETION_CODESTRAL = "Text-Completion-Codestral", Tencent = "Tencent", TogetherAI = "TogetherAI", TOPAZ = "Topaz", Triton = "Triton", + TypeSafe = "TypeSafe", V0 = "V0", VERCEL_AI_GATEWAY = "Vercel Ai Gateway", Vertex_AI = "Vertex AI (Anthropic, Gemini, etc.)", @@ -287,11 +291,13 @@ export const provider_map: Record = { SCX_AI: "scx-ai", Snowflake: "snowflake", Soniox: "soniox", + StrandsDecider: "strands_decider", TEXT_COMPLETION_CODESTRAL: "text-completion-codestral", Tencent: "tencent", TogetherAI: "together_ai", TOPAZ: "topaz", Triton: "triton", + TypeSafe: "typesafe", V0: "v0", VERCEL_AI_GATEWAY: "vercel_ai_gateway", Vertex_AI: "vertex_ai", @@ -387,11 +393,13 @@ export const providerLogoMap: Partial> = { [Providers.SCX_AI]: scxAiLogo.src, [Providers.Snowflake]: snowflakeLogo.src, [Providers.Soniox]: sonioxLogo.src, + [Providers.StrandsDecider]: strandsLogo.src, [Providers.Tencent]: tencentLogo.src, [Providers.TEXT_COMPLETION_CODESTRAL]: mistralLogo.src, [Providers.TogetherAI]: togetheraiLogo.src, [Providers.TOPAZ]: topazLogo.src, [Providers.Triton]: nvidiaTritonLogo.src, + [Providers.TypeSafe]: typesafeLogo.src, [Providers.V0]: v0Logo.src, [Providers.VERCEL_AI_GATEWAY]: vercelLogo.src, [Providers.Vertex_AI]: googleLogo.src, @@ -457,7 +465,9 @@ const providerPlaceholderMap: Partial> = { [Providers.Sail]: "sail/openai/gpt-oss-120b", [Providers.SCX_AI]: "scx-ai/GLM-5.2", [Providers.Snowflake]: "snowflake/mistral-7b", + [Providers.StrandsDecider]: "strands_decider/strands-decider-2B-hobson-v19", [Providers.Tencent]: "tencent/deepseek-v4-pro", + [Providers.TypeSafe]: "typesafe/jev-latest", [Providers.Vertex_AI]: "gemini-pro", [Providers.VolcEngine]: "volcengine/", [Providers.Voyage]: "voyage/",