diff --git a/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.test.tsx b/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.test.tsx index 6190b6aef78..0208acc841f 100644 --- a/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.test.tsx +++ b/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.test.tsx @@ -54,6 +54,32 @@ describe("prepareModelAddRequest", () => { expect(deployment.litellmParamsObj.custom_llm_provider).toBe("petals"); }); + it("sends the backend's own slug for a provider whose key is spelled differently in provider_map", async () => { + const formValues = { + model_mappings: [{ public_name: "Composer", litellm_model: "cursor/composer-1" }], + model_name: "cursor/composer-1", + custom_llm_provider: "CURSOR", + }; + + const deployments = await prepareModelAddRequest({ ...formValues }, "token", null); + + expect(deployments![0].litellmParamsObj.custom_llm_provider).toBe("cursor"); + }); + + it("builds the wildcard model from the backend slug, not the raw dropdown key", async () => { + const formValues = { + model: ["all-wildcard"], + custom_llm_provider: "CURSOR", + }; + + const deployments = await prepareModelAddRequest({ ...formValues }, "token", null); + + expect(deployments).toHaveLength(1); + const [deployment] = deployments!; + expect(deployment.modelName).toBe("cursor/*"); + expect(deployment.litellmParamsObj.model).toBe("cursor/*"); + }); + it("ignores litellm_credential_name inside LiteLLM Params JSON", async () => { const formValues = { model_mappings: [ diff --git a/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.tsx b/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.tsx index 41133958c0a..5f2747e0415 100644 --- a/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.tsx +++ b/ui/litellm-dashboard/src/components/add_model/handle_add_model_submit.tsx @@ -1,6 +1,6 @@ import { toast } from "@/lib/toast"; import { Model, modelCreateCall } from "../networking"; -import { provider_map } from "../provider_info_helpers"; +import { resolveLitellmProviderSlug } from "../provider_info_helpers"; import { ptuPickerToUtcIso } from "../../utils/ptuDatetime"; export const prepareModelAddRequest = async (formValues: Record, accessToken: string, form: any) => { @@ -14,9 +14,7 @@ export const prepareModelAddRequest = async (formValues: Record, ac // Handle wildcard case if (formValues["model"] && formValues["model"].includes("all-wildcard")) { const customProviderKey = formValues["custom_llm_provider"] as string; - const mappedProvider = - provider_map[customProviderKey as keyof typeof provider_map] ?? customProviderKey.toLowerCase(); - const litellm_custom_provider = mappedProvider; + const litellm_custom_provider = resolveLitellmProviderSlug(customProviderKey); const wildcardModel = litellm_custom_provider + "/*"; formValues["model_name"] = wildcardModel; modelMappings.push({ @@ -101,9 +99,7 @@ export const prepareModelAddRequest = async (formValues: Record, ac if (key == "model_name") { litellmParamsObj["model"] = value; } else if (key == "custom_llm_provider") { - const providerKey = value as string; - const mappingResult = provider_map[providerKey as keyof typeof provider_map] ?? providerKey.toLowerCase(); - litellmParamsObj["custom_llm_provider"] = mappingResult; + litellmParamsObj["custom_llm_provider"] = resolveLitellmProviderSlug(value as string); } else if (key == "model") { continue; } 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 7de8605e92f..728f95a2225 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.test.tsx @@ -1,4 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import { readFileSync } from "fs"; +import { resolve } from "path"; import { Providers, getPlaceholder, @@ -6,8 +8,17 @@ import { getProviderModels, providerLogoMap, provider_map, + resolveLitellmProviderSlug, } from "./provider_info_helpers"; +const PROVIDER_CREATE_FIELDS: { provider: string; litellm_provider: string }[] = JSON.parse( + readFileSync(resolve(__dirname, "../../../../litellm/proxy/public_endpoints/provider_create_fields.json"), "utf8"), +); + +const BUNDLED_MODEL_MAP: Record = JSON.parse( + readFileSync(resolve(__dirname, "../../../../model_prices_and_context_window.json"), "utf8"), +); + describe("provider_info_helpers", () => { it("maps Microsoft 365 Copilot to its chat model provider and placeholder", () => { expect(provider_map.MICROSOFT_365_COPILOT).toBe("microsoft_365_copilot"); @@ -364,6 +375,40 @@ describe("provider_info_helpers", () => { }); }); + describe("resolveLitellmProviderSlug", () => { + it("should resolve every provider the backend serves to the slug that backend declares", () => { + const misresolved = PROVIDER_CREATE_FIELDS.filter( + (entry) => resolveLitellmProviderSlug(entry.provider) !== entry.litellm_provider, + ).map( + (entry) => + `${entry.provider} -> ${resolveLitellmProviderSlug(entry.provider)} (want ${entry.litellm_provider})`, + ); + expect(misresolved).toEqual([]); + }); + + it("should resolve providers the backend spells in caps and provider_map spells in camel case", () => { + expect(resolveLitellmProviderSlug("MINIMAX")).toBe("minimax"); + expect(resolveLitellmProviderSlug("CURSOR")).toBe("cursor"); + expect(resolveLitellmProviderSlug("RUNWAYML")).toBe("runwayml"); + }); + + it("should resolve providers absent from provider_map to their lowercased value", () => { + expect(resolveLitellmProviderSlug("MILVUS")).toBe("milvus"); + expect(resolveLitellmProviderSlug("LANGFUSE")).toBe("langfuse"); + expect(resolveLitellmProviderSlug("LITELLM_PROXY")).toBe("litellm_proxy"); + }); + + it("should keep SAGEMAKER on the plain slug rather than the chat variant SageMaker maps to", () => { + expect(resolveLitellmProviderSlug("SAGEMAKER")).toBe("sagemaker"); + expect(resolveLitellmProviderSlug("SageMaker")).toBe("sagemaker_chat"); + }); + + it("should prefer an exact provider_map key over the lowercase fallback", () => { + expect(resolveLitellmProviderSlug("Vertex_AI")).toBe("vertex_ai"); + expect(resolveLitellmProviderSlug("Google_AI_Studio")).toBe("gemini"); + }); + }); + describe("getProviderModels", () => { it("should return empty array when provider is not provided", () => { const modelMap = {}; @@ -587,6 +632,34 @@ describe("provider_info_helpers", () => { expect(result).toEqual([]); }); + it("should populate models for a provider whose backend key is spelled differently from its provider_map key", () => { + const modelMap = { + "cursor/composer-1": { litellm_provider: "cursor" }, + "gpt-4": { litellm_provider: "openai" }, + }; + const result = getProviderModels("CURSOR" as Providers, modelMap); + expect(result).toEqual(["cursor/composer-1"]); + }); + + it("should populate models for SAGEMAKER, whose key is absent from provider_map", () => { + const modelMap = { + "sagemaker-base": { litellm_provider: "sagemaker" }, + "gpt-4": { litellm_provider: "openai" }, + }; + expect(getProviderModels("SAGEMAKER" as Providers, modelMap)).toEqual(["sagemaker-base"]); + }); + + it("should populate MiniMax's bundled models from the real cost map", () => { + const minimaxModels = Object.keys(BUNDLED_MODEL_MAP).filter( + (key) => BUNDLED_MODEL_MAP[key]?.litellm_provider === "minimax", + ); + expect(minimaxModels.length).toBeGreaterThan(0); + + const result = getProviderModels("MINIMAX" as Providers, BUNDLED_MODEL_MAP); + + expect([...result].sort()).toEqual([...minimaxModels].sort()); + }); + it("should handle multiple providers correctly", () => { const modelMap = { "gpt-3.5-turbo": { litellm_provider: "openai" }, diff --git a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx index fca4e5bc74d..fa774d46f0f 100644 --- a/ui/litellm-dashboard/src/components/provider_info_helpers.tsx +++ b/ui/litellm-dashboard/src/components/provider_info_helpers.tsx @@ -318,6 +318,9 @@ export const provider_map: Record = { const standaloneSubproviderSlugs = new Set(["bedrock_mantle"]); +export const resolveLitellmProviderSlug = (provider: string): string => + provider_map[provider] ?? provider.toLowerCase(); + export const providerLogoMap: Partial> = { [Providers.A2A_Agent]: a2aAgentLogo.src, [Providers.AI21]: ai21Logo.src, @@ -487,7 +490,7 @@ export const getPlaceholder = (selectedProvider: string): string => { export const getProviderModels = (provider: string, modelMap: any): Array => { let providerKey = provider; - let custom_llm_provider = provider_map[providerKey]; + let custom_llm_provider = providerKey ? resolveLitellmProviderSlug(providerKey) : undefined; let providerModels: Array = [];