mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
fix: Add array type checks for model, agent, and MCP hub data to prevent crashes from non-array API responses and include a regression test.
This PR:Fixes a frontend regression where the
PublicModelHub
page would crash with TypeError: e.filter is not a function when the API returned an error object (e.g. { "detail": "..." }) instead of the expected data array.
Changes:
Added defensive Array.isArray() checks in
src/components/public_model_hub.tsx
for:
modelHubData
agentHubData
mcpHubData
Updated useMemo hooks and helper functions to handle invalid data gracefully.
Added a regression test in
src/components/public_model_hub.test.tsx
that mocks a non-array API response to ensure the component renders without crashing.
This commit is contained in:
parent
02ed25c886
commit
2c430ebd96
2 changed files with 51 additions and 21 deletions
|
|
@ -38,10 +38,10 @@ beforeAll(() => {
|
|||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: () => { },
|
||||
removeListener: () => { },
|
||||
addEventListener: () => { },
|
||||
removeEventListener: () => { },
|
||||
addListener: () => {},
|
||||
removeListener: () => {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => false,
|
||||
}),
|
||||
});
|
||||
|
|
@ -171,4 +171,18 @@ describe("PublicModelHub", () => {
|
|||
expect(unknownStatus).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it("handles non-array response gracefully (regression test for e.filter crash)", async () => {
|
||||
const networkingModule = await import("./networking");
|
||||
// Mock the API to return an object (like an error response) instead of an array
|
||||
vi.mocked(networkingModule.modelHubPublicModelsCall).mockResolvedValue({
|
||||
detail: "No models configured",
|
||||
} as any);
|
||||
|
||||
render(<PublicModelHub />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("navbar")).toBeInTheDocument();
|
||||
expect(screen.getByText("Model Hub")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
};
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
if (!modelHubData) return [];
|
||||
if (!modelHubData || !Array.isArray(modelHubData)) return [];
|
||||
|
||||
let searchResults = modelHubData;
|
||||
|
||||
|
|
@ -324,7 +324,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
}, [modelHubData, searchTerm, selectedProviders, selectedModes, selectedFeatures]);
|
||||
|
||||
const filteredAgentData = useMemo(() => {
|
||||
if (!agentHubData) return [];
|
||||
if (!agentHubData || !Array.isArray(agentHubData)) return [];
|
||||
|
||||
let searchResults = agentHubData;
|
||||
|
||||
|
|
@ -375,7 +375,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
}, [agentHubData, agentSearchTerm, selectedAgentSkills]);
|
||||
|
||||
const filteredMcpData = useMemo(() => {
|
||||
if (!mcpHubData) return [];
|
||||
if (!mcpHubData || !Array.isArray(mcpHubData)) return [];
|
||||
|
||||
let searchResults = mcpHubData;
|
||||
|
||||
|
|
@ -696,18 +696,29 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
const original = row.original;
|
||||
const tagColor = original.health_status === "healthy" ? "green" : original.health_status === "unhealthy" ? "red" : "default";
|
||||
const responseTimeLabel = original.health_response_time ? `Response Time: ${Number(original.health_response_time).toFixed(2)}ms` : "N/A";
|
||||
const lastCheckedLabel = original.health_checked_at ? `Last Checked: ${new Date(original.health_checked_at).toLocaleString()}` : "N/A";
|
||||
const tagColor =
|
||||
original.health_status === "healthy" ? "green" : original.health_status === "unhealthy" ? "red" : "default";
|
||||
const responseTimeLabel = original.health_response_time
|
||||
? `Response Time: ${Number(original.health_response_time).toFixed(2)}ms`
|
||||
: "N/A";
|
||||
const lastCheckedLabel = original.health_checked_at
|
||||
? `Last Checked: ${new Date(original.health_checked_at).toLocaleString()}`
|
||||
: "N/A";
|
||||
|
||||
return <Tooltip title={<>
|
||||
<div>
|
||||
{responseTimeLabel}
|
||||
</div>
|
||||
<div>
|
||||
{lastCheckedLabel}
|
||||
</div>
|
||||
</>}><Tag key={original.model_group} color={tagColor}><span className="capitalize">{original.health_status ?? "Unknown"}</span></Tag></Tooltip>;
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
<div>{responseTimeLabel}</div>
|
||||
<div>{lastCheckedLabel}</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Tag key={original.model_group} color={tagColor}>
|
||||
<span className="capitalize">{original.health_status ?? "Unknown"}</span>
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
size: 100,
|
||||
},
|
||||
|
|
@ -963,7 +974,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
accessToken={accessToken || null}
|
||||
isPublicPage={true}
|
||||
isDarkMode={false}
|
||||
toggleDarkMode={() => { }}
|
||||
toggleDarkMode={() => {}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
@ -1092,6 +1103,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
}}
|
||||
>
|
||||
{modelHubData &&
|
||||
Array.isArray(modelHubData) &&
|
||||
getUniqueProviders(modelHubData).map((provider) => (
|
||||
<Select.Option key={provider} value={provider}>
|
||||
{provider}
|
||||
|
|
@ -1111,6 +1123,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
allowClear
|
||||
>
|
||||
{modelHubData &&
|
||||
Array.isArray(modelHubData) &&
|
||||
getUniqueModes(modelHubData).map((mode) => (
|
||||
<Select.Option key={mode} value={mode}>
|
||||
{mode}
|
||||
|
|
@ -1130,6 +1143,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
allowClear
|
||||
>
|
||||
{modelHubData &&
|
||||
Array.isArray(modelHubData) &&
|
||||
getUniqueFeatures(modelHubData).map((feature) => (
|
||||
<Select.Option key={feature} value={feature}>
|
||||
{feature}
|
||||
|
|
@ -1154,7 +1168,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
</TabPane>
|
||||
|
||||
{/* Agents Tab */}
|
||||
{agentHubData && agentHubData.length > 0 && (
|
||||
{agentHubData && Array.isArray(agentHubData) && agentHubData.length > 0 && (
|
||||
<TabPane tab="Agent Hub" key="agents">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<Title className="text-2xl font-semibold text-gray-900">Available Agents</Title>
|
||||
|
|
@ -1192,6 +1206,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
allowClear
|
||||
>
|
||||
{agentHubData &&
|
||||
Array.isArray(agentHubData) &&
|
||||
getUniqueAgentSkills(agentHubData).map((skill) => (
|
||||
<Select.Option key={skill} value={skill}>
|
||||
{skill}
|
||||
|
|
@ -1217,7 +1232,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
)}
|
||||
|
||||
{/* MCP Servers Tab */}
|
||||
{mcpHubData && mcpHubData.length > 0 && (
|
||||
{mcpHubData && Array.isArray(mcpHubData) && mcpHubData.length > 0 && (
|
||||
<TabPane tab="MCP Hub" key="mcp">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<Title className="text-2xl font-semibold text-gray-900">Available MCP Servers</Title>
|
||||
|
|
@ -1255,6 +1270,7 @@ const PublicModelHub: React.FC<PublicModelHubProps> = ({ accessToken, isEmbedded
|
|||
allowClear
|
||||
>
|
||||
{mcpHubData &&
|
||||
Array.isArray(mcpHubData) &&
|
||||
getUniqueMcpTransports(mcpHubData).map((transport) => (
|
||||
<Select.Option key={transport} value={transport}>
|
||||
{transport}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue