Change useful_links to include index for UI

This commit is contained in:
yuneng-jiang 2025-12-11 21:25:40 -08:00
parent 7ea407b239
commit d1e53365a8
7 changed files with 80 additions and 31 deletions

View file

@ -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" = (

View file

@ -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):

View file

@ -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):

View file

@ -221,7 +221,8 @@ export interface PublicModelHubInfo {
docs_title: string;
custom_docs_description: string | null;
litellm_version: string;
useful_links: Record<string, string>;
// Supports both old format (Record<string, string>) and new format (Record<string, {url: string, index: number}>)
useful_links: Record<string, string | { url: string; index: number }>;
}
export interface LiteLLMWellKnownUiConfig {
@ -2362,7 +2363,10 @@ export const modelExceptionsCall = async (
}
};
export const updateUsefulLinksCall = async (accessToken: string, useful_links: Record<string, string>) => {
export const updateUsefulLinksCall = async (
accessToken: string,
useful_links: Record<string, string | { url: string; index: number }>,
) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/model_hub/update_useful_links` : `/model_hub/update_useful_links`;
const response = await fetch(url, {

View file

@ -97,7 +97,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
const [pageTitle, setPageTitle] = useState<string>("LiteLLM Gateway");
const [customDocsDescription, setCustomDocsDescription] = useState<string | null>(null);
const [litellmVersion, setLitellmVersion] = useState<string>("");
const [usefulLinks, setUsefulLinks] = useState<Record<string, string>>({});
const [usefulLinks, setUsefulLinks] = useState<Record<string, string | { url: string; index: number }>>({});
const [loading, setLoading] = useState<boolean>(true);
const [agentLoading, setAgentLoading] = useState<boolean>(true);
const [mcpLoading, setMcpLoading] = useState<boolean>(true);
@ -976,16 +976,24 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
<Card className="mb-10 p-8 bg-white border border-gray-200 rounded-lg shadow-sm">
<Title className="text-2xl font-semibold mb-6 text-gray-900">Useful Links</Title>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Object.entries(usefulLinks || {}).map(([title, url]) => (
<button
key={title}
onClick={() => window.open(url, "_blank")}
className="flex items-center space-x-3 text-blue-600 hover:text-blue-800 transition-colors p-3 rounded-lg hover:bg-blue-50 border border-gray-200"
>
<ExternalLinkIcon className="w-4 h-4" />
<Text className="text-sm font-medium">{title}</Text>
</button>
))}
{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 }) => (
<button
key={title}
onClick={() => window.open(url, "_blank")}
className="flex items-center space-x-3 text-blue-600 hover:text-blue-800 transition-colors p-3 rounded-lg hover:bg-blue-50 border border-gray-200"
>
<ExternalLinkIcon className="w-4 h-4" />
<Text className="text-sm font-medium">{title}</Text>
</button>
))}
</div>
</Card>
)}

View file

@ -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 },
}),
);

View file

@ -16,6 +16,7 @@ interface Link {
id: string;
displayName: string;
url: string;
index?: number;
}
const UsefulLinksManagement: React.FC<UsefulLinksManagementProps> = ({ accessToken, userRole }) => {
@ -38,11 +39,32 @@ const UsefulLinksManagement: React.FC<UsefulLinksManagementProps> = ({ 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<UsefulLinksManagementProps> = ({ accessTok
if (!accessToken) return false;
try {
// Convert array back to object format
const linksObject: Record<string, string> = {};
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<string, { url: string; index: number }> = {};
updatedLinks.forEach((link, index) => {
linksObject[link.displayName] = {
url: link.url,
index: index,
};
});
await updateUsefulLinksCall(accessToken, linksObject);