From d1e53365a859d3a4210adf0c2bd9ef377f1587fc Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 11 Dec 2025 21:25:40 -0800 Subject: [PATCH] Change useful_links to include index for UI --- litellm/__init__.py | 5 ++- .../model_management_endpoints.py | 7 ++- .../public_endpoints/public_endpoints.py | 7 ++- .../src/components/networking.tsx | 8 +++- .../src/components/public_model_hub.tsx | 30 ++++++++----- .../useful_links_management.test.tsx | 10 +++-- .../components/useful_links_management.tsx | 44 +++++++++++++++---- 7 files changed, 80 insertions(+), 31 deletions(-) diff --git a/litellm/__init__.py b/litellm/__init__.py index 2c6f04a3aef..ef44aa53a13 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -399,7 +399,10 @@ disable_copilot_system_to_assistant: bool = ( public_mcp_servers: Optional[List[str]] = None public_model_groups: Optional[List[str]] = None public_agent_groups: Optional[List[str]] = None -public_model_groups_links: Dict[str, str] = {} +# Supports both old format (Dict[str, str]) and new format (Dict[str, Dict[str, Any]]) +# New format: { "displayName": { "url": "...", "index": 0 } } +# Old format: { "displayName": "url" } (for backward compatibility) +public_model_groups_links: Dict[str, Union[str, Dict[str, Any]]] = {} #### REQUEST PRIORITIZATION ####### priority_reservation: Optional[Dict[str, Union[float, PriorityReservationDict]]] = None priority_reservation_settings: "PriorityReservationSettings" = ( diff --git a/litellm/types/proxy/management_endpoints/model_management_endpoints.py b/litellm/types/proxy/management_endpoints/model_management_endpoints.py index cb9dcc63e21..a8ff3971305 100644 --- a/litellm/types/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/types/proxy/management_endpoints/model_management_endpoints.py @@ -1,4 +1,4 @@ -from typing import Dict, List +from typing import Dict, List, Union, Any from pydantic import BaseModel, Field @@ -10,7 +10,10 @@ class ModelGroupInfoProxy(ModelGroupInfo): class UpdateUsefulLinksRequest(BaseModel): - useful_links: Dict[str, str] + # Supports both old format (Dict[str, str]) and new format (Dict[str, Dict[str, Any]]) + # New format: { "displayName": { "url": "...", "index": 0 } } + # Old format: { "displayName": "url" } (for backward compatibility) + useful_links: Dict[str, Union[str, Dict[str, Any]]] class NewModelGroupRequest(BaseModel): diff --git a/litellm/types/proxy/public_endpoints/public_endpoints.py b/litellm/types/proxy/public_endpoints/public_endpoints.py index eeb1b10fe61..57d68771c7f 100644 --- a/litellm/types/proxy/public_endpoints/public_endpoints.py +++ b/litellm/types/proxy/public_endpoints/public_endpoints.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Literal, Optional +from typing import Dict, List, Literal, Optional, Union, Any from pydantic import BaseModel @@ -7,7 +7,10 @@ class PublicModelHubInfo(BaseModel): docs_title: str custom_docs_description: Optional[str] litellm_version: str - useful_links: Optional[Dict[str, str]] + # Supports both old format (Dict[str, str]) and new format (Dict[str, Dict[str, Any]]) + # New format: { "displayName": { "url": "...", "index": 0 } } + # Old format: { "displayName": "url" } (for backward compatibility) + useful_links: Optional[Dict[str, Union[str, Dict[str, Any]]]] class ProviderCredentialField(BaseModel): diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index acd448b3536..65c41c5aab4 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -221,7 +221,8 @@ export interface PublicModelHubInfo { docs_title: string; custom_docs_description: string | null; litellm_version: string; - useful_links: Record; + // Supports both old format (Record) and new format (Record) + useful_links: Record; } export interface LiteLLMWellKnownUiConfig { @@ -2362,7 +2363,10 @@ export const modelExceptionsCall = async ( } }; -export const updateUsefulLinksCall = async (accessToken: string, useful_links: Record) => { +export const updateUsefulLinksCall = async ( + accessToken: string, + useful_links: Record, +) => { try { const url = proxyBaseUrl ? `${proxyBaseUrl}/model_hub/update_useful_links` : `/model_hub/update_useful_links`; const response = await fetch(url, { diff --git a/ui/litellm-dashboard/src/components/public_model_hub.tsx b/ui/litellm-dashboard/src/components/public_model_hub.tsx index 6235a0c66f6..3493f0bf93f 100644 --- a/ui/litellm-dashboard/src/components/public_model_hub.tsx +++ b/ui/litellm-dashboard/src/components/public_model_hub.tsx @@ -97,7 +97,7 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded const [pageTitle, setPageTitle] = useState("LiteLLM Gateway"); const [customDocsDescription, setCustomDocsDescription] = useState(null); const [litellmVersion, setLitellmVersion] = useState(""); - const [usefulLinks, setUsefulLinks] = useState>({}); + const [usefulLinks, setUsefulLinks] = useState>({}); const [loading, setLoading] = useState(true); const [agentLoading, setAgentLoading] = useState(true); const [mcpLoading, setMcpLoading] = useState(true); @@ -976,16 +976,24 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded Useful Links
- {Object.entries(usefulLinks || {}).map(([title, url]) => ( - - ))} + {Object.entries(usefulLinks || {}) + .map(([title, value]) => { + // Handle both old format (string) and new format ({url, index}) + const url = typeof value === "string" ? value : value.url; + const index = typeof value === "string" ? 0 : value.index ?? 0; + return { title, url, index }; + }) + .sort((a, b) => a.index - b.index) + .map(({ title, url }) => ( + + ))}
)} diff --git a/ui/litellm-dashboard/src/components/useful_links_management.test.tsx b/ui/litellm-dashboard/src/components/useful_links_management.test.tsx index 7b1a40a499a..cf7c6083e2c 100644 --- a/ui/litellm-dashboard/src/components/useful_links_management.test.tsx +++ b/ui/litellm-dashboard/src/components/useful_links_management.test.tsx @@ -64,7 +64,9 @@ describe("UsefulLinksManagement", () => { await user.click(screen.getByRole("button", { name: /add link/i })); await waitFor(() => - expect(mockedUpdateUsefulLinksCall).toHaveBeenCalledWith("token", { Docs: "https://docs.example.com" }), + expect(mockedUpdateUsefulLinksCall).toHaveBeenCalledWith("token", { + Docs: { url: "https://docs.example.com", index: 0 }, + }), ); expect(await screen.findByText("Docs")).toBeInTheDocument(); @@ -98,9 +100,9 @@ describe("UsefulLinksManagement", () => { await waitFor(() => expect(mockedUpdateUsefulLinksCall).toHaveBeenCalledWith("token", { - "Second Link": "https://second.example.com", - "First Link": "https://first.example.com", - "Third Link": "https://third.example.com", + "Second Link": { url: "https://second.example.com", index: 0 }, + "First Link": { url: "https://first.example.com", index: 1 }, + "Third Link": { url: "https://third.example.com", index: 2 }, }), ); diff --git a/ui/litellm-dashboard/src/components/useful_links_management.tsx b/ui/litellm-dashboard/src/components/useful_links_management.tsx index a9367812f4b..19ef4605d87 100644 --- a/ui/litellm-dashboard/src/components/useful_links_management.tsx +++ b/ui/litellm-dashboard/src/components/useful_links_management.tsx @@ -16,6 +16,7 @@ interface Link { id: string; displayName: string; url: string; + index?: number; } const UsefulLinksManagement: React.FC = ({ accessToken, userRole }) => { @@ -38,11 +39,32 @@ const UsefulLinksManagement: React.FC = ({ accessTok const usefulLinks = response.useful_links || {}; // Convert object to array of links with ids - const linksArray = Object.entries(usefulLinks).map(([displayName, url], index) => ({ - id: `${index}-${displayName}`, - displayName, - url: url as string, - })); + // Handle both old format (Dict[str, str]) and new format (Dict[str, {url, index}]) + const linksArray = Object.entries(usefulLinks) + .map(([displayName, value]) => { + // Check if it's the new format with {url, index} + if (typeof value === "object" && value !== null && "url" in value) { + return { + id: `${(value as any).index ?? 0}-${displayName}`, + displayName, + url: (value as any).url as string, + index: (value as any).index ?? 0, + }; + } else { + // Old format: just a string URL + return { + id: `0-${displayName}`, + displayName, + url: value as string, + index: 0, + }; + } + }) + .sort((a, b) => (a.index ?? 0) - (b.index ?? 0)) + .map((link, index) => ({ + ...link, + id: `${index}-${link.displayName}`, + })); setLinks(linksArray); } else { @@ -69,10 +91,14 @@ const UsefulLinksManagement: React.FC = ({ accessTok if (!accessToken) return false; try { - // Convert array back to object format - const linksObject: Record = {}; - updatedLinks.forEach((link) => { - linksObject[link.displayName] = link.url; + // Convert array back to object format with index for ordering + // New format: { "displayName": { "url": "...", "index": 0 } } + const linksObject: Record = {}; + updatedLinks.forEach((link, index) => { + linksObject[link.displayName] = { + url: link.url, + index: index, + }; }); await updateUsefulLinksCall(accessToken, linksObject);