From 14683b502b5e0107c9a3375d72c8af7f1cfbc21c Mon Sep 17 00:00:00 2001 From: ishaan-j-berri Date: Fri, 6 Nov 2026 17:01:56 +0800 Subject: [PATCH] fix(ui): support model_info.logo_url in Model Management (#50056) (#50371) --- .../components/AllModelsTable.test.tsx | 16 ++++++++ .../components/ModelsTableColumns.tsx | 6 ++- .../src/components/model_dashboard/types.ts | 1 + .../model_info_view.integration.test.tsx | 16 ++++++++ .../src/components/model_info_view.tsx | 10 ++++- .../molecules/models/ProviderLogo.test.tsx | 38 +++++++++++++++++++ .../molecules/models/ProviderLogo.tsx | 10 +++-- 7 files changed, 90 insertions(+), 7 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTable.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTable.test.tsx index b00affc50f5..3de187cbf55 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTable.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTable.test.tsx @@ -132,6 +132,22 @@ describe("AllModelsTable", () => { expect(onModelIdClick).toHaveBeenCalledWith("model-1"); }); + it("should display the model metadata logo for a gateway-routed model", () => { + const logoUrl = "https://cdn.example.com/gateway.svg"; + render( + , + ); + + expect(screen.getByRole("img", { name: "openai logo" })).toHaveAttribute("src", logoUrl); + }); + it("opens the team detail from the team ID cell", async () => { const user = userEvent.setup(); const onTeamIdClick = vi.fn(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelsTableColumns.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelsTableColumns.tsx index 0e286418355..65fd8be97c2 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelsTableColumns.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/ModelsTableColumns.tsx @@ -70,7 +70,7 @@ function ModelInformationCell({ model, displayName }: { model: ModelData; displa } > {model.provider ? ( - + ) : ( - @@ -88,7 +88,9 @@ function ModelInformationCell({ model, displayName }: { model: ModelData; displa
- {model.provider ? : null} + {model.provider ? ( + + ) : null} {model.provider || t("modelsAndEndpoints.modelsTableColumns.unknownProvider")} diff --git a/ui/litellm-dashboard/src/components/model_dashboard/types.ts b/ui/litellm-dashboard/src/components/model_dashboard/types.ts index 47c7eab8ba6..2a8ec028aff 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/types.ts +++ b/ui/litellm-dashboard/src/components/model_dashboard/types.ts @@ -5,6 +5,7 @@ export interface PerSecondCostTier { export interface ModelInfo { id: string; + logo_url?: string | null; created_at: string; updated_at: string; created_by: string; diff --git a/ui/litellm-dashboard/src/components/model_info_view.integration.test.tsx b/ui/litellm-dashboard/src/components/model_info_view.integration.test.tsx index a42db06d251..404ef6e7f20 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.integration.test.tsx @@ -205,6 +205,22 @@ describe("ModelInfoView", () => { }); }); + it("should display the model metadata logo in the provider details", async () => { + const logoUrl = "https://cdn.example.com/gateway.svg"; + mockUseModelsInfo.mockReturnValue({ + data: { + data: [{ ...defaultModelData, model_info: { ...defaultModelData.model_info, logo_url: logoUrl } }], + }, + isLoading: false, + error: null, + }); + + render(, { wrapper }); + + expect(await screen.findByRole("img", { name: "openai logo" })).toHaveAttribute("src", logoUrl); + expect(screen.getByRole("heading", { name: "openai" })).toBeInTheDocument(); + }); + it("shows Kubernetes pods for discovery-enabled models", async () => { mockUseModelsInfo.mockReturnValue({ data: { diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx index efa7caf550d..5a52006632c 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -45,7 +45,7 @@ import { tagListCall, testConnectionRequest, } from "./networking"; -import { Logo } from "@/components/molecules/logo/Logo"; +import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; import { ModelPricingSummary } from "@/components/molecules/models/ModelPricingSummary"; import UpdateModelCredentialsModal from "./update_model_credentials_modal"; import ModelInfoEditForm, { type ModelEditSubmitValues, type TouchedPricingField } from "./ModelInfoEditForm"; @@ -626,7 +626,13 @@ export default function ModelInfoView({

{t("modelInfoView.overview.provider")}

- {modelData.provider && } + {modelData.provider && ( + + )}

{modelData.provider || t("modelInfoView.notSet")}

diff --git a/ui/litellm-dashboard/src/components/molecules/models/ProviderLogo.test.tsx b/ui/litellm-dashboard/src/components/molecules/models/ProviderLogo.test.tsx index 9a9c631890b..b42719510ea 100644 --- a/ui/litellm-dashboard/src/components/molecules/models/ProviderLogo.test.tsx +++ b/ui/litellm-dashboard/src/components/molecules/models/ProviderLogo.test.tsx @@ -5,10 +5,15 @@ import { ProviderLogo } from "./ProviderLogo"; import * as providerInfoHelpers from "../../provider_info_helpers"; vi.mock("../../provider_info_helpers"); +vi.mock("@/lib/serverRootPath", () => ({ serverRootPath: "/litellm" })); describe("ProviderLogo", () => { beforeEach(() => { vi.resetAllMocks(); + vi.mocked(providerInfoHelpers.getProviderLogoAndName).mockReturnValue({ + logo: "/assets/provider-logo.svg", + displayName: "Test Provider", + }); }); it("should render the component", () => { @@ -20,6 +25,39 @@ describe("ProviderLogo", () => { expect(screen.getByText("t")).toBeInTheDocument(); }); + it.each([ + ["https://cdn.example.com/gateway.svg", "https://cdn.example.com/gateway.svg"], + ["/ui/assets/logos/gateway.svg", "/litellm/ui/assets/logos/gateway.svg"], + ])("should display a custom logo from %s", (logoUrl, expectedSrc) => { + render(); + + expect(screen.getByRole("img", { name: "test logo" })).toHaveAttribute("src", expectedSrc); + }); + + it.each([undefined, null, ""])("should use the provider logo when the override is %s", (logoUrl) => { + render(); + + expect(screen.getByRole("img", { name: "test logo" })).toHaveAttribute("src", "/assets/provider-logo.svg"); + }); + + it("should recover when a failed custom logo is replaced", () => { + const { rerender } = render(); + + act(() => { + fireEvent.error(screen.getByRole("img", { name: "test logo" })); + }); + + expect(screen.queryByRole("img")).not.toBeInTheDocument(); + expect(screen.getByText("t")).toBeInTheDocument(); + + rerender(); + + expect(screen.getByRole("img", { name: "test logo" })).toHaveAttribute( + "src", + "https://cdn.example.com/gateway.svg", + ); + }); + it("should show fallback when image fails to load", () => { vi.mocked(providerInfoHelpers.getProviderLogoAndName).mockReturnValue({ logo: "/path/to/logo.png", diff --git a/ui/litellm-dashboard/src/components/molecules/models/ProviderLogo.tsx b/ui/litellm-dashboard/src/components/molecules/models/ProviderLogo.tsx index 4bc11126ae4..eb93dd77e30 100644 --- a/ui/litellm-dashboard/src/components/molecules/models/ProviderLogo.tsx +++ b/ui/litellm-dashboard/src/components/molecules/models/ProviderLogo.tsx @@ -3,9 +3,13 @@ import { Logo } from "@/components/molecules/logo/Logo"; interface ProviderLogoProps { provider: string; + logoUrl?: string | null; className?: string; } -export const ProviderLogo: React.FC = ({ provider, className = "w-4 h-4" }) => ( - -); +export const ProviderLogo: React.FC = ({ provider, logoUrl, className = "w-4 h-4" }) => + logoUrl ? ( + + ) : ( + + );