mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
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:
parent
dcf1b445e6
commit
95a0c30a05
34 changed files with 214 additions and 54 deletions
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[] = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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[] = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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). */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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={{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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's a preview of what Audit Logs offer:
|
||||
</p>
|
||||
<img
|
||||
src={auditLogsPreviewImg}
|
||||
src={resolveLogoSrc(auditLogsPreviewImg)}
|
||||
alt="Audit Logs Preview"
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
|
|
|
|||
60
ui/litellm-dashboard/src/lib/assetPaths.test.ts
Normal file
60
ui/litellm-dashboard/src/lib/assetPaths.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
27
ui/litellm-dashboard/src/lib/assetPaths.ts
Normal file
27
ui/litellm-dashboard/src/lib/assetPaths.ts
Normal 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);
|
||||
};
|
||||
|
|
@ -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}`;
|
||||
|
|
|
|||
12
ui/litellm-dashboard/src/lib/serverRootPath.ts
Normal file
12
ui/litellm-dashboard/src/lib/serverRootPath.ts
Normal 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;
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue