fix(ui): render logos under a custom server_root_path (#31156)

The App Router migration moved pages to deeper path segments and the proxy
can be mounted under a sub-path (e.g. /litellm behind a reverse proxy). Local
logo asset paths were emitted without the server root prefix, so they resolved
off the origin root and 404'd. Route every local logo src through a single
resolver that prefixes the live server root path and leaves external URLs
untouched, fixing provider, guardrail, vector store, callback, MCP and
audit-log logos at any route depth and root path.
This commit is contained in:
ryan-crabbe-berri 2026-06-24 17:13:10 -07:00 committed by Yuneng Jiang
parent dcf1b445e6
commit 95a0c30a05
No known key found for this signature in database
34 changed files with 214 additions and 54 deletions

View file

@ -3,6 +3,7 @@ import { TextInput, Button } from "@tremor/react";
import { Select as AntdSelect, Form, Tooltip, Radio } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Providers, provider_map, providerLogoMap } from "@/components/provider_info_helpers";
import { resolveLogoSrc } from "@/lib/assetPaths";
import { MarginConfig } from "./types";
import { handleImageError } from "./provider_display_helpers";
@ -73,7 +74,7 @@ const AddMarginForm: React.FC<AddMarginFormProps> = ({
<AntdSelect.Option key={providerEnum} value={providerEnum} label={providerDisplayName}>
<div className="flex items-center space-x-2">
<img
src={providerLogoMap[providerDisplayName]}
src={resolveLogoSrc(providerLogoMap[providerDisplayName])}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => handleImageError(e, providerDisplayName)}

View file

@ -3,6 +3,7 @@ import { TextInput, Button } from "@tremor/react";
import { Select as AntdSelect, Form, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Providers, provider_map, providerLogoMap } from "@/components/provider_info_helpers";
import { resolveLogoSrc } from "@/lib/assetPaths";
import { DiscountConfig } from "./types";
import { handleImageError } from "./provider_display_helpers";
@ -60,7 +61,7 @@ const AddProviderForm: React.FC<AddProviderFormProps> = ({
<AntdSelect.Option key={providerEnum} value={providerEnum} label={providerDisplayName}>
<div className="flex items-center space-x-2">
<img
src={providerLogoMap[providerDisplayName]}
src={resolveLogoSrc(providerLogoMap[providerDisplayName])}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => handleImageError(e, providerDisplayName)}

View file

@ -1,4 +1,5 @@
import { Providers, provider_map, providerLogoMap } from "@/components/provider_info_helpers";
import { resolveLogoSrc } from "@/lib/assetPaths";
export interface ProviderDisplayInfo {
displayName: string;
@ -16,7 +17,7 @@ export const getProviderDisplayInfo = (providerValue: string): ProviderDisplayIn
if (enumKey) {
const displayName = Providers[enumKey as keyof typeof Providers];
const logo = providerLogoMap[displayName];
const logo = resolveLogoSrc(providerLogoMap[displayName]) ?? "";
return { displayName, logo, enumKey };
}

View file

@ -3,8 +3,8 @@ import { InfoCircleOutlined } from "@ant-design/icons";
import { useQuery } from "@tanstack/react-query";
import { Button, TextInput } from "@tremor/react";
import { Form, Input, Modal, Select, Tooltip, Typography } from "antd";
import Image from "next/image";
import React, { useState } from "react";
import { resolveLogoSrc } from "@/lib/assetPaths";
import NotificationsManager from "../molecules/notifications_manager";
import { createSearchTool, fetchAvailableSearchProviders } from "../networking";
import SearchConnectionTest from "./SearchConnectionTest";
@ -13,7 +13,7 @@ import { AvailableSearchProvider, SearchTool } from "./types";
const { TextArea } = Input;
// Search provider logos folder path (matches existing provider logo pattern)
const searchProviderLogosFolder = "../ui/assets/logos/";
const searchProviderLogosFolder = "/ui/assets/logos/";
// Helper function to get logo path for a search provider
const getSearchProviderLogo = (providerName: string): string => {
@ -28,12 +28,13 @@ interface SearchProviderLabelProps {
const SearchProviderLabel: React.FC<SearchProviderLabelProps> = ({ providerName, displayName }) => (
<div style={{ display: "flex", alignItems: "center" }}>
<Image
src={getSearchProviderLogo(providerName)}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={resolveLogoSrc(getSearchProviderLogo(providerName))}
alt=""
width={20}
height={20}
style={{
width: "20px",
height: "20px",
marginRight: "8px",
objectFit: "contain",
}}

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { Modal, Form, Select, Input, Steps, Radio, Tag, Divider, Switch, InputNumber, Collapse } from "antd";
import MessageManager from "@/components/molecules/message_manager";
import { resolveLogoSrc } from "@/lib/assetPaths";
import { Button } from "@tremor/react";
import { CheckCircleFilled, KeyOutlined, RobotOutlined, AppstoreOutlined, InfoCircleOutlined } from "@ant-design/icons";
import CreatedKeyDisplay from "../shared/CreatedKeyDisplay";
@ -711,13 +712,17 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
value={info.agent_type}
label={
<div className="flex items-center gap-2">
<img src={info.logo_url || ""} alt="" className="w-4 h-4 object-contain" />
<img src={resolveLogoSrc(info.logo_url) ?? ""} alt="" className="w-4 h-4 object-contain" />
<span>{info.agent_type_display_name}</span>
</div>
}
>
<div className="flex items-center gap-3 py-1">
<img src={info.logo_url || ""} alt={info.agent_type_display_name} className="w-5 h-5 object-contain" />
<img
src={resolveLogoSrc(info.logo_url) ?? ""}
alt={info.agent_type_display_name}
className="w-5 h-5 object-contain"
/>
<div>
<div className="font-medium">{info.agent_type_display_name}</div>
{info.description && <div className="text-xs text-gray-500">{info.description}</div>}
@ -942,7 +947,9 @@ const AddAgentForm: React.FC<AddAgentFormProps> = ({ visible, onClose, accessTok
<Modal
title={
<div className="flex items-center space-x-3 pb-4 border-b border-gray-100">
{selectedLogo && currentStep < 1 && <img src={selectedLogo} alt="Agent" className="w-6 h-6 object-contain" />}
{selectedLogo && currentStep < 1 && (
<img src={resolveLogoSrc(selectedLogo)} alt="Agent" className="w-6 h-6 object-contain" />
)}
<h2 className="text-xl font-semibold text-gray-900">Add New Agent</h2>
</div>
}

View file

@ -7,7 +7,7 @@ interface CallbackConfig {
description: string;
}
const asset_logos_folder = "../ui/assets/logos/";
const asset_logos_folder = "/ui/assets/logos/";
export const CALLBACK_CONFIGS: CallbackConfig[] = [
{

View file

@ -20,6 +20,7 @@ import {
shouldRenderLLMJudgeFields,
shouldRenderPIIConfigSettings,
} from "./guardrail_info_helpers";
import { resolveLogoSrc } from "@/lib/assetPaths";
import GuardrailOptionalParams from "./guardrail_optional_params";
import GuardrailProviderFields from "./guardrail_provider_fields";
import LLMJudgeFields from "./llm_judge/LLMJudgeFields";
@ -689,7 +690,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
<div style={{ display: "flex", alignItems: "center" }}>
{guardrailLogoMap[value] && (
<img
src={guardrailLogoMap[value]}
src={resolveLogoSrc(guardrailLogoMap[value])}
alt=""
style={{
height: "20px",
@ -710,7 +711,7 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
<div style={{ display: "flex", alignItems: "center" }}>
{guardrailLogoMap[value] && (
<img
src={guardrailLogoMap[value]}
src={resolveLogoSrc(guardrailLogoMap[value])}
alt=""
style={{
height: "20px",

View file

@ -8,6 +8,7 @@ import {
type SkipSystemMessageChoice,
type SkipToolMessageChoice,
} from "./guardrail_info_helpers";
import { resolveLogoSrc } from "@/lib/assetPaths";
import { getGuardrailUISettings, getGlobalLitellmHeaderName } from "../networking";
import PiiConfiguration from "./pii_configuration";
import NotificationsManager from "../molecules/notifications_manager";
@ -392,7 +393,7 @@ const EditGuardrailForm: React.FC<EditGuardrailFormProps> = ({
<div style={{ display: "flex", alignItems: "center" }}>
{guardrailLogoMap[value] && (
<img
src={guardrailLogoMap[value]}
src={resolveLogoSrc(guardrailLogoMap[value])}
alt=""
style={{
height: "20px",

View file

@ -1,6 +1,7 @@
import React, { useState } from "react";
import { CheckCircleFilled } from "@ant-design/icons";
import { GuardrailCardInfo } from "./guardrail_garden_data";
import { resolveLogoSrc } from "@/lib/assetPaths";
const LogoWithFallback: React.FC<{ src: string; name: string }> = ({ src, name }) => {
const [hasError, setHasError] = useState(false);
@ -29,7 +30,7 @@ const LogoWithFallback: React.FC<{ src: string; name: string }> = ({ src, name }
return (
<img
src={src}
src={resolveLogoSrc(src)}
alt=""
style={{ width: 28, height: 28, borderRadius: 6, objectFit: "contain", flexShrink: 0 }}
onError={() => setHasError(true)}

View file

@ -16,7 +16,7 @@ export interface GuardrailCardInfo {
providerKey?: string;
}
const ASSET_PREFIX = "../ui/assets/logos/";
const ASSET_PREFIX = "/ui/assets/logos/";
export const LITELLM_CONTENT_FILTER_CARDS: GuardrailCardInfo[] = [
{

View file

@ -2,6 +2,7 @@ import React, { useState } from "react";
import { Button } from "antd";
import { ArrowLeftOutlined } from "@ant-design/icons";
import AddGuardrailForm from "./add_guardrail_form";
import { resolveLogoSrc } from "@/lib/assetPaths";
import { GUARDRAIL_PRESETS } from "./guardrail_garden_configs";
import { GuardrailCardInfo } from "./guardrail_garden_data";
@ -60,7 +61,7 @@ const GuardrailDetailView: React.FC<GuardrailDetailViewProps> = ({ card, onBack,
{/* ── Header block (Vertex-style) ── */}
<div style={{ display: "flex", alignItems: "center", gap: 16, marginBottom: 8 }}>
<img
src={card.logo}
src={resolveLogoSrc(card.logo)}
alt=""
style={{ width: 40, height: 40, borderRadius: 8, objectFit: "contain" }}
onError={(e) => {

View file

@ -1,3 +1,5 @@
import { resolveLogoSrc } from "@/lib/assetPaths";
// Legacy enum - keeping for backward compatibility
export enum GuardrailProviders {
PresidioPII = "Presidio PII",
@ -113,7 +115,7 @@ export const shouldRenderLLMJudgeFields = (provider: string | null) => {
return guardrail_provider_map[provider] === "llm_as_a_judge";
};
const asset_logos_folder = "../ui/assets/logos/";
const asset_logos_folder = "/ui/assets/logos/";
export const guardrailLogoMap: Record<string, string> = {
"Zscaler AI Guard": `${asset_logos_folder}zscaler.svg`,
@ -163,9 +165,9 @@ export const getGuardrailLogoAndName = (guardrailValue: string): { logo: string;
// Get the display name from current GuardrailProviders and logo from map
const currentProviders = getGuardrailProviders();
const displayName = currentProviders[enumKey as keyof typeof currentProviders];
const logo = guardrailLogoMap[displayName as keyof typeof guardrailLogoMap];
const logo = resolveLogoSrc(guardrailLogoMap[displayName as keyof typeof guardrailLogoMap]) ?? "";
return { logo: logo || "", displayName: displayName || guardrailValue };
return { logo, displayName: displayName || guardrailValue };
};
/** Tri-state UI value for `litellm_params.skip_system_message_in_guardrail` (inherit = use global). */

View file

@ -2,6 +2,7 @@ import React from "react";
import { Tag } from "antd";
import { CogIcon, BanIcon } from "@heroicons/react/outline";
import { callbackInfo, callback_map, reverse_callback_map } from "./callback_info_helpers";
import { resolveLogoSrc } from "@/lib/assetPaths";
interface LoggingConfig {
callback_name: string;
@ -68,7 +69,7 @@ export function LoggingSettingsView({
<div className="space-y-3">
{loggingConfigs.map((config, index) => {
const displayName = getLoggingDisplayName(config.callback_name);
const logoUrl = callbackInfo[displayName]?.logo;
const logoUrl = resolveLogoSrc(callbackInfo[displayName]?.logo);
return (
<div
@ -114,7 +115,7 @@ export function LoggingSettingsView({
{disabledCallbacks.map((callbackName, index) => {
// Handle both display names and internal values
const displayName = reverse_callback_map[callbackName] || callbackName;
const logoUrl = callbackInfo[displayName]?.logo;
const logoUrl = resolveLogoSrc(callbackInfo[displayName]?.logo);
return (
<div

View file

@ -1,6 +1,7 @@
import React, { useState } from "react";
import { Input, Tooltip } from "antd";
import { InfoCircleOutlined, LinkOutlined } from "@ant-design/icons";
import { resolveLogoSrc } from "@/lib/assetPaths";
const logos = "/ui/assets/logos/";
@ -56,7 +57,7 @@ const MCPLogoSelector: React.FC<MCPLogoSelectorProps> = ({ value, onChange }) =>
{value && (
<div className="flex items-center gap-3 mb-3 p-3 bg-gray-50 rounded-lg border border-gray-200">
<img
src={value}
src={resolveLogoSrc(value)}
alt="Selected logo"
className="w-10 h-10 object-contain rounded"
onError={(e) => {
@ -96,7 +97,7 @@ const MCPLogoSelector: React.FC<MCPLogoSelectorProps> = ({ value, onChange }) =>
style={{ width: 40, height: 40 }}
>
<img
src={logo.url}
src={resolveLogoSrc(logo.url)}
alt={logo.name}
className="w-5 h-5 object-contain"
onError={() => handleImgError(logo.url)}

View file

@ -1,6 +1,7 @@
import React from "react";
import { Button, TextInput } from "@tremor/react";
import { MCPTool, InputSchema, InputSchemaProperty } from "./types";
import { resolveLogoSrc } from "@/lib/assetPaths";
import { Form, Select, Tooltip } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import NotificationsManager from "../molecules/notifications_manager";
@ -301,7 +302,7 @@ export function ToolTestPanel({
{tool.mcp_info.logo_url && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={tool.mcp_info.logo_url}
src={resolveLogoSrc(tool.mcp_info.logo_url)}
alt={`${tool.mcp_info.server_name} logo`}
className="w-6 h-6 object-contain"
/>

View file

@ -29,8 +29,9 @@ import NotificationsManager from "../molecules/notifications_manager";
import { useMcpOAuthFlow } from "@/hooks/useMcpOAuthFlow";
import { useTestMCPConnection } from "@/hooks/useTestMCPConnection";
import { getSecureItem, setSecureItem } from "@/utils/secureStorage";
import { resolveLogoSrc } from "@/lib/assetPaths";
const asset_logos_folder = "../ui/assets/logos/";
const asset_logos_folder = "/ui/assets/logos/";
export const mcpLogoImg = `${asset_logos_folder}mcp_logo.png`;
interface CreateMCPServerProps {
@ -586,7 +587,7 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
</button>
)}
<img
src={mcpLogoImg}
src={resolveLogoSrc(mcpLogoImg)}
alt="MCP Logo"
className="w-8 h-8 object-contain"
style={{

View file

@ -3,6 +3,7 @@ import { Modal, Input, Typography } from "antd";
import { fetchDiscoverableMCPServers } from "../networking";
import { DiscoverableMCPServer, DiscoverMCPServersResponse } from "./types";
import { mcpLogoImg } from "./create_mcp_server";
import { resolveLogoSrc } from "@/lib/assetPaths";
const { Search } = Input;
const { Text } = Typography;
@ -95,7 +96,7 @@ const MCPDiscovery: React.FC<MCPDiscoveryProps> = ({
<div className="flex items-center justify-between pb-4 border-b border-gray-100">
<div className="flex items-center space-x-3">
<img
src={mcpLogoImg}
src={resolveLogoSrc(mcpLogoImg)}
alt="MCP Logo"
className="w-8 h-8 object-contain"
style={{
@ -242,7 +243,7 @@ const MCPDiscovery: React.FC<MCPDiscoveryProps> = ({
>
{server.icon_url ? (
<img
src={server.icon_url}
src={resolveLogoSrc(server.icon_url)}
alt={server.title}
style={{
width: 20,

View file

@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useState } from "react";
import { useQuery, useMutation } from "@tanstack/react-query";
import { ToolTestPanel } from "./ToolTestPanel";
import { resolveLogoSrc } from "@/lib/assetPaths";
import { MCPTool, MCPToolsViewerProps, MCPContent, CallMCPToolResponse, getMcpOAuthMode } from "./types";
import { listMCPTools, callMCPTool, getMCPOAuthUserCredentialStatus } from "../networking";
import { isTokenValid, getToken, removeToken } from "@/utils/mcpTokenStore";
@ -484,7 +485,7 @@ const MCPToolsViewer = ({
<div className="flex items-start space-x-2">
{tool.mcp_info.logo_url && (
<img
src={tool.mcp_info.logo_url}
src={resolveLogoSrc(tool.mcp_info.logo_url)}
alt={`${tool.mcp_info.server_name} logo`}
className="w-4 h-4 object-contain flex-shrink-0 mt-0.5"
/>

View file

@ -4,6 +4,7 @@ import type { UploadProps } from "antd/es/upload";
import React, { useState } from "react";
import ProviderSpecificFields from "../add_model/provider_specific_fields";
import { Providers, providerLogoMap } from "../provider_info_helpers";
import { resolveLogoSrc } from "@/lib/assetPaths";
import { resetCredentialFormOnProviderChange } from "./credential_form_helpers";
const { Link } = Typography;
@ -67,7 +68,7 @@ const AddCredentialsModal: React.FC<AddCredentialsModalProps> = ({ open, onCance
<AntdSelect.Option key={providerEnum} value={providerEnum}>
<div className="flex items-center space-x-2">
<img
src={providerLogoMap[providerDisplayName]}
src={resolveLogoSrc(providerLogoMap[providerDisplayName])}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => {

View file

@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
import ProviderSpecificFields from "../add_model/provider_specific_fields";
import { CredentialItem } from "../networking";
import { Providers, providerLogoMap } from "../provider_info_helpers";
import { resolveLogoSrc } from "@/lib/assetPaths";
import { resetCredentialFormOnProviderChange } from "./credential_form_helpers";
const { Link } = Typography;
@ -100,7 +101,7 @@ export default function EditCredentialsModal({
<AntdSelect.Option key={providerEnum} value={providerEnum}>
<div className="flex items-center space-x-2">
<img
src={providerLogoMap[providerDisplayName]}
src={resolveLogoSrc(providerLogoMap[providerDisplayName])}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => {

View file

@ -32,6 +32,9 @@ import NotificationsManager from "./molecules/notifications_manager";
import type { MCPUserEnvVarsStatus } from "./mcp_tools/types";
import { createApiClient, deriveErrorMessage } from "@/lib/http/client";
import { resolveApiBase } from "@/lib/http/resolveApiBase";
import { serverRootPath, setServerRootPath } from "@/lib/serverRootPath";
export { serverRootPath };
export { deriveErrorMessage };
export { ApiError } from "@/lib/http/client";
@ -46,8 +49,6 @@ const resolveDefaultBase = (fallback: string | null): string | null =>
? "http://localhost:4000"
: fallback;
const defaultProxyBaseUrl = resolveDefaultBase(null);
const defaultServerRootPath = "/";
export let serverRootPath = defaultServerRootPath;
const WORKER_URL_KEY = "litellm_worker_url";
// If a worker URL is in localStorage, use it as the initial proxyBaseUrl.
// This survives page navigation and the sessionStorage.clear() in user_dashboard.
@ -92,7 +93,7 @@ const updateProxyBaseUrl = (serverRootPath: string, receivedProxyBaseUrl: string
};
const updateServerRootPath = (receivedServerRootPath: string) => {
serverRootPath = receivedServerRootPath;
setServerRootPath(receivedServerRootPath);
};
export const getProxyBaseUrl = (): string => {

View file

@ -403,3 +403,27 @@ describe("provider_info_helpers", () => {
});
});
});
describe("getProviderLogoAndName under a custom server_root_path", () => {
afterEach(() => {
vi.resetModules();
vi.doUnmock("@/lib/serverRootPath");
});
// Regression: under SERVER_ROOT_PATH=/litellm the logo must be requested at
// /litellm/ui/assets/logos/... A bare /ui/... path is served off the root and
// 404s behind the reverse proxy.
it("prefixes the server root path onto the resolved logo", async () => {
vi.resetModules();
vi.doMock("@/lib/serverRootPath", () => ({ serverRootPath: "/litellm" }));
const { getProviderLogoAndName } = await import("./provider_info_helpers");
expect(getProviderLogoAndName("openai").logo).toBe("/litellm/ui/assets/logos/openai_small.svg");
});
it("leaves the logo at /ui/... when mounted at the root", async () => {
vi.resetModules();
vi.doMock("@/lib/serverRootPath", () => ({ serverRootPath: "/" }));
const { getProviderLogoAndName } = await import("./provider_info_helpers");
expect(getProviderLogoAndName("openai").logo).toBe("/ui/assets/logos/openai_small.svg");
});
});

View file

@ -1,3 +1,5 @@
import { resolveLogoSrc } from "@/lib/assetPaths";
export enum Providers {
A2A_Agent = "A2A Agent",
AI21 = "Ai21",
@ -314,7 +316,7 @@ export const getProviderLogoAndName = (providerValue: string): { logo: string; d
// Handle special case for "gemini" provider value
if (providerValue.toLowerCase() === "gemini") {
const displayName = Providers.Google_AI_Studio;
const logo = providerLogoMap[displayName];
const logo = resolveLogoSrc(providerLogoMap[displayName]) ?? "";
return { logo, displayName };
}
@ -329,7 +331,7 @@ export const getProviderLogoAndName = (providerValue: string): { logo: string; d
// Get the display name from Providers enum and logo from map
const displayName = Providers[enumKey as keyof typeof Providers];
const logo = providerLogoMap[displayName as keyof typeof providerLogoMap];
const logo = resolveLogoSrc(providerLogoMap[displayName as keyof typeof providerLogoMap]) ?? "";
return { logo, displayName };
};

View file

@ -22,6 +22,7 @@ import React, { useEffect, useState } from "react";
import { Button as Button2, Form, Input, Modal, Select, Typography } from "antd";
import EmailSettings from "./email_settings";
import { resolveLogoSrc } from "@/lib/assetPaths";
import NotificationsManager from "./molecules/notifications_manager";
const { Title, Paragraph } = Typography;
@ -53,7 +54,7 @@ interface genericCallbackParams {
litellm_callback_params: string[] | null; // known required params for this callback
}
const assetsLogoFolder = "../ui/assets/logos/";
const assetsLogoFolder = "/ui/assets/logos/";
interface DynamicParamsFieldsProps {
params: string[];
@ -156,10 +157,11 @@ const CallbackSelector: React.FC<CallbackSelectorProps> = ({
>
{callbackConfigs.map((callbackConfig) => {
const logo = callbackConfig.logo;
const logoSrc =
const logoSrc = resolveLogoSrc(
logo && (logo.includes("/") || logo.startsWith("data:") || logo.startsWith("http"))
? logo
: `${assetsLogoFolder}${logo}`;
: `${assetsLogoFolder}${logo}`,
);
return (
<SelectItem key={callbackConfig.id} value={callbackConfig.id}>

View file

@ -6,6 +6,7 @@ import { InfoCircleOutlined } from "@ant-design/icons";
import { Button, Card, TextInput } from "@tremor/react";
import { PlusIcon, TrashIcon, CogIcon, BanIcon } from "@heroicons/react/outline";
import { callbackInfo, callback_map, mapDisplayToInternalNames } from "../callback_info_helpers";
import { resolveLogoSrc } from "@/lib/assetPaths";
import NumericalInput from "../shared/numerical_input";
const { Option } = Select;
@ -178,7 +179,7 @@ const LoggingSettings: React.FC<LoggingSettingsProps> = ({
optionLabelProp="label"
>
{allCallbacks.map((callbackName) => {
const logo = callbackInfo[callbackName]?.logo;
const logo = resolveLogoSrc(callbackInfo[callbackName]?.logo);
const description = callbackInfo[callbackName]?.description;
return (
<Option key={callbackName} value={callbackName} label={callbackName}>
@ -244,7 +245,7 @@ const LoggingSettings: React.FC<LoggingSettingsProps> = ({
const callbackDisplayName = config.callback_name
? Object.entries(callback_map).find(([_, value]) => value === config.callback_name)?.[0]
: undefined;
const logoUrl = callbackDisplayName ? callbackInfo[callbackDisplayName]?.logo : null;
const logoUrl = callbackDisplayName ? resolveLogoSrc(callbackInfo[callbackDisplayName]?.logo) : null;
return (
<Card
@ -282,7 +283,7 @@ const LoggingSettings: React.FC<LoggingSettingsProps> = ({
optionLabelProp="label"
>
{supportedCallbacks.map((callbackName) => {
const logo = callbackInfo[callbackName]?.logo;
const logo = resolveLogoSrc(callbackInfo[callbackName]?.logo);
const description = callbackInfo[callbackName]?.description;
return (
<Option key={callbackName} value={callbackName} label={callbackName}>

View file

@ -14,6 +14,7 @@ import {
getProviderSpecificFields,
VectorStoreFieldConfig,
} from "../vector_store_providers";
import { resolveLogoSrc } from "@/lib/assetPaths";
import NotificationsManager from "../molecules/notifications_manager";
import S3VectorsConfig from "./S3VectorsConfig";
@ -294,7 +295,7 @@ const CreateVectorStore: React.FC<CreateVectorStoreProps> = ({ accessToken, onSu
<Select.Option key={providerEnum} value={vectorStoreProviderMap[providerEnum]}>
<div className="flex items-center space-x-2">
<img
src={vectorStoreProviderLogoMap[providerDisplayName]}
src={resolveLogoSrc(vectorStoreProviderLogoMap[providerDisplayName])}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => {

View file

@ -10,6 +10,7 @@ import {
getProviderSpecificFields,
VectorStoreFieldConfig,
} from "../vector_store_providers";
import { resolveLogoSrc } from "@/lib/assetPaths";
import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models";
import NotificationsManager from "../molecules/notifications_manager";
@ -130,7 +131,7 @@ const VectorStoreForm: React.FC<VectorStoreFormProps> = ({
<Select.Option key={providerEnum} value={vectorStoreProviderMap[providerEnum]}>
<div className="flex items-center space-x-2">
<img
src={vectorStoreProviderLogoMap[providerDisplayName]}
src={resolveLogoSrc(vectorStoreProviderLogoMap[providerDisplayName])}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => {

View file

@ -6,6 +6,7 @@ import { ArrowLeftIcon } from "@heroicons/react/outline";
import { vectorStoreInfoCall, vectorStoreUpdateCall, credentialListCall, CredentialItem } from "../networking";
import { VectorStore } from "./types";
import { Providers, providerLogoMap, provider_map } from "../provider_info_helpers";
import { resolveLogoSrc } from "@/lib/assetPaths";
import VectorStoreTester from "./VectorStoreTester";
import NotificationsManager from "../molecules/notifications_manager";
@ -177,7 +178,7 @@ const VectorStoreInfoView: React.FC<VectorStoreInfoViewProps> = ({
<Select2.Option key={providerEnum} value={provider_map[providerEnum]}>
<div className="flex items-center space-x-2">
<img
src={providerLogoMap[providerDisplayName]}
src={resolveLogoSrc(providerLogoMap[providerDisplayName])}
alt={`${providerEnum} logo`}
className="w-5 h-5"
onError={(e) => {
@ -299,7 +300,7 @@ const VectorStoreInfoView: React.FC<VectorStoreInfoViewProps> = ({
// Get the display name from Providers enum and logo from map
const displayName = Providers[enumKey as keyof typeof Providers];
const logo = providerLogoMap[displayName];
const logo = resolveLogoSrc(providerLogoMap[displayName]) ?? "";
return { displayName, logo };
})();

View file

@ -1,3 +1,5 @@
import { resolveLogoSrc } from "@/lib/assetPaths";
export enum VectorStoreProviders {
Bedrock = "Amazon Bedrock",
S3Vectors = "Amazon S3 Vectors",
@ -20,7 +22,7 @@ export const vectorStoreProviderMap: Record<string, string> = {
S3Vectors: "s3_vectors",
};
const asset_logos_folder = "../ui/assets/logos/";
const asset_logos_folder = "/ui/assets/logos/";
export const vectorStoreProviderLogoMap: Record<string, string> = {
[VectorStoreProviders.Bedrock]: `${asset_logos_folder}bedrock.svg`,
@ -214,7 +216,7 @@ export const getVectorStoreProviderLogoAndName = (providerValue: string): { logo
// Get the display name from VectorStoreProviders enum and logo from map
const displayName = VectorStoreProviders[enumKey as keyof typeof VectorStoreProviders];
const logo = vectorStoreProviderLogoMap[displayName as keyof typeof vectorStoreProviderLogoMap];
const logo = resolveLogoSrc(vectorStoreProviderLogoMap[displayName as keyof typeof vectorStoreProviderLogoMap]) ?? "";
return { logo, displayName };
};

View file

@ -3,6 +3,7 @@ import { useQuery, keepPreviousData } from "@tanstack/react-query";
import { Table, Tag, Input, Select, Button, Pagination, Spin } from "antd";
import { ReloadOutlined, LoadingOutlined } from "@ant-design/icons";
import type { ColumnsType } from "antd/es/table";
import { resolveLogoSrc } from "@/lib/assetPaths";
import moment from "moment";
import { uiAuditLogsCall } from "../networking";
import { AuditLogEntry } from "./columns";
@ -20,7 +21,7 @@ interface AuditLogsProps {
premiumUser: boolean;
}
const asset_logos_folder = "../ui/assets/";
const asset_logos_folder = "/ui/assets/";
export const auditLogsPreviewImg = `${asset_logos_folder}audit-logs-preview.png`;
const TABLE_NAME_DISPLAY: Record<string, string> = {
@ -151,7 +152,7 @@ export default function AuditLogs({ userID, userRole, token, accessToken, isActi
Here&apos;s a preview of what Audit Logs offer:
</p>
<img
src={auditLogsPreviewImg}
src={resolveLogoSrc(auditLogsPreviewImg)}
alt="Audit Logs Preview"
style={{
maxWidth: "100%",

View file

@ -0,0 +1,60 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { withServerRoot } from "./assetPaths";
describe("withServerRoot", () => {
it("leaves a root-relative path unchanged at the default root", () => {
expect(withServerRoot("/ui/assets/logos/openai.svg", "/")).toBe("/ui/assets/logos/openai.svg");
expect(withServerRoot("/ui/assets/logos/openai.svg", "")).toBe("/ui/assets/logos/openai.svg");
});
it("prefixes a custom server root path", () => {
expect(withServerRoot("/ui/assets/logos/openai.svg", "/litellm")).toBe("/litellm/ui/assets/logos/openai.svg");
});
it("normalizes a trailing or missing slash in the root", () => {
expect(withServerRoot("/ui/assets/logos/x.svg", "/litellm/")).toBe("/litellm/ui/assets/logos/x.svg");
expect(withServerRoot("/ui/assets/logos/x.svg", "team-x")).toBe("/team-x/ui/assets/logos/x.svg");
});
it("guarantees a single leading slash on the path", () => {
expect(withServerRoot("ui/assets/logos/x.svg", "/litellm")).toBe("/litellm/ui/assets/logos/x.svg");
});
});
describe("resolveLogoSrc", () => {
afterEach(() => {
vi.resetModules();
vi.doUnmock("@/lib/serverRootPath");
});
const importWithRoot = async (root: string) => {
vi.resetModules();
vi.doMock("@/lib/serverRootPath", () => ({ serverRootPath: root }));
return import("./assetPaths");
};
it("returns undefined for empty values", async () => {
const { resolveLogoSrc } = await importWithRoot("/litellm");
expect(resolveLogoSrc(null)).toBeUndefined();
expect(resolveLogoSrc(undefined)).toBeUndefined();
expect(resolveLogoSrc("")).toBeUndefined();
});
it("passes external and inline sources through untouched", async () => {
const { resolveLogoSrc } = await importWithRoot("/litellm");
expect(resolveLogoSrc("https://www.zapier.com/logo.png")).toBe("https://www.zapier.com/logo.png");
expect(resolveLogoSrc("data:image/png;base64,AAAA")).toBe("data:image/png;base64,AAAA");
expect(resolveLogoSrc("//cdn.example.com/x.svg")).toBe("//cdn.example.com/x.svg");
});
it("roots a local asset path using the live server root path", async () => {
const { resolveLogoSrc } = await importWithRoot("/litellm");
expect(resolveLogoSrc("/ui/assets/logos/github.svg")).toBe("/litellm/ui/assets/logos/github.svg");
});
it("leaves a local asset path unchanged at the default root", async () => {
const { resolveLogoSrc } = await importWithRoot("/");
expect(resolveLogoSrc("/ui/assets/logos/github.svg")).toBe("/ui/assets/logos/github.svg");
});
});

View file

@ -0,0 +1,27 @@
import { serverRootPath } from "@/lib/serverRootPath";
import { normalizeRootPath } from "@/lib/http/resolveApiBase";
const EXTERNAL_SRC = /^(https?:|data:|blob:|\/\/)/i;
/**
* Prefix a root-relative asset path (e.g. "/ui/assets/logos/openai.svg") with the
* proxy's server root path so it resolves when the UI is mounted under a sub-path
* (e.g. "/litellm" behind a reverse proxy). At the default root it returns the
* path unchanged.
*/
export const withServerRoot = (path: string, root: string): string => {
const prefix = normalizeRootPath(root);
return `${prefix}${path.startsWith("/") ? path : `/${path}`}`;
};
/**
* Resolve a logo `src` for rendering. External URLs (http(s), data, blob,
* protocol-relative) are returned untouched; local asset paths are prefixed with
* the live server root path. `root` is read at call time so it reflects the value
* `getUiConfig()` resolves asynchronously after load.
*/
export const resolveLogoSrc = (value: string | null | undefined, root: string = serverRootPath): string | undefined => {
if (!value) return undefined;
if (EXTERNAL_SRC.test(value)) return value;
return withServerRoot(value, root);
};

View file

@ -12,7 +12,7 @@ export interface ApiBaseInputs {
serverRootPath?: string | null;
}
const normalizeRootPath = (serverRootPath: string | null | undefined): string => {
export const normalizeRootPath = (serverRootPath: string | null | undefined): string => {
const trimmed = (serverRootPath ?? "").trim();
if (trimmed === "" || trimmed === "/") return "";
const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;

View file

@ -0,0 +1,12 @@
/**
* Canonical holder for the proxy's server root path (e.g. "/litellm" when mounted
* behind a reverse proxy, "/" at the root). Kept in a tiny leaf module so url/asset
* helpers can read it without importing and forcing every test to mock the large
* networking module. `networking` re-exports `serverRootPath` and updates it via
* `setServerRootPath` from its UI-config bootstrap.
*/
export let serverRootPath = "/";
export const setServerRootPath = (rootPath: string): void => {
serverRootPath = rootPath;
};