mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-05 14:23:44 +00:00
refactor: remove some unused files and add tests
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
/* @vitest-environment jsdom */
|
||||
import React from "react";
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useMCPAccessGroups } from "./useMCPAccessGroups";
|
||||
import * as networking from "@/components/networking";
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
fetchMCPAccessGroups: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
|
||||
default: vi.fn(() => ({
|
||||
accessToken: "test-token-456",
|
||||
})),
|
||||
}));
|
||||
|
||||
const createQueryClient = () =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const queryClient = createQueryClient();
|
||||
return React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
|
||||
const mockAccessGroups = ["group-1", "group-2", "group-3"];
|
||||
|
||||
describe("useMCPAccessGroups", () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: "test-token-456",
|
||||
} as any);
|
||||
});
|
||||
|
||||
it("should return MCP access groups when access token is present", async () => {
|
||||
vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue(mockAccessGroups);
|
||||
|
||||
const { result } = renderHook(() => useMCPAccessGroups(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(networking.fetchMCPAccessGroups).toHaveBeenCalledWith("test-token-456");
|
||||
expect(result.current.data).toEqual(mockAccessGroups);
|
||||
});
|
||||
|
||||
it("should not fetch when access token is not available", async () => {
|
||||
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: null,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useMCPAccessGroups(), { wrapper });
|
||||
|
||||
expect(result.current.status).toBe("pending");
|
||||
expect(networking.fetchMCPAccessGroups).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should expose error state when fetch fails", async () => {
|
||||
const mockError = new Error("Failed to fetch MCP access groups");
|
||||
vi.mocked(networking.fetchMCPAccessGroups).mockRejectedValue(mockError);
|
||||
|
||||
const { result } = renderHook(() => useMCPAccessGroups(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.error).toEqual(mockError);
|
||||
});
|
||||
|
||||
it("should return empty array when API returns no groups", async () => {
|
||||
vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]);
|
||||
|
||||
const { result } = renderHook(() => useMCPAccessGroups(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/* @vitest-environment jsdom */
|
||||
import React from "react";
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useMCPServers } from "./useMCPServers";
|
||||
import * as networking from "@/components/networking";
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
fetchMCPServers: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../useAuthorized", () => ({
|
||||
default: vi.fn(() => ({
|
||||
accessToken: "test-token-123",
|
||||
})),
|
||||
}));
|
||||
|
||||
const createQueryClient = () =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
gcTime: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
const queryClient = createQueryClient();
|
||||
return React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||
};
|
||||
|
||||
const mockServers = [
|
||||
{
|
||||
server_id: "server-1",
|
||||
server_name: "Server One",
|
||||
url: "http://localhost:4000",
|
||||
created_at: "2025-01-01T00:00:00Z",
|
||||
created_by: "user-1",
|
||||
updated_at: "2025-01-01T00:00:00Z",
|
||||
updated_by: "user-1",
|
||||
},
|
||||
];
|
||||
|
||||
describe("useMCPServers", () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
const useAuthorizedModule = await import("../useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: "test-token-123",
|
||||
} as any);
|
||||
});
|
||||
|
||||
it("should return MCP servers when access token is present", async () => {
|
||||
vi.mocked(networking.fetchMCPServers).mockResolvedValue(mockServers);
|
||||
|
||||
const { result } = renderHook(() => useMCPServers(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(networking.fetchMCPServers).toHaveBeenCalledWith("test-token-123");
|
||||
expect(result.current.data).toEqual(mockServers);
|
||||
});
|
||||
|
||||
it("should not fetch when access token is not available", async () => {
|
||||
const useAuthorizedModule = await import("../useAuthorized");
|
||||
vi.mocked(useAuthorizedModule.default).mockReturnValue({
|
||||
accessToken: null,
|
||||
} as any);
|
||||
|
||||
const { result } = renderHook(() => useMCPServers(), { wrapper });
|
||||
|
||||
expect(result.current.status).toBe("pending");
|
||||
expect(networking.fetchMCPServers).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should expose error state when fetch fails", async () => {
|
||||
const mockError = new Error("Failed to fetch MCP servers");
|
||||
vi.mocked(networking.fetchMCPServers).mockRejectedValue(mockError);
|
||||
|
||||
const { result } = renderHook(() => useMCPServers(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.error).toEqual(mockError);
|
||||
});
|
||||
|
||||
it("should return empty array when API returns empty list", async () => {
|
||||
vi.mocked(networking.fetchMCPServers).mockResolvedValue([]);
|
||||
|
||||
const { result } = renderHook(() => useMCPServers(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.data).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,103 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Select, SelectItem, Text, Title } from "@tremor/react";
|
||||
import { ProxySettings, UserInfo } from "./user_dashboard";
|
||||
import { getProxyUISettings } from "./networking";
|
||||
|
||||
interface DashboardTeamProps {
|
||||
teams: object[] | null;
|
||||
setSelectedTeam: React.Dispatch<React.SetStateAction<any | null>>;
|
||||
userRole: string | null;
|
||||
proxySettings: ProxySettings | null;
|
||||
setProxySettings: React.Dispatch<React.SetStateAction<ProxySettings | null>>;
|
||||
userInfo: UserInfo | null;
|
||||
accessToken: string | null;
|
||||
setKeys: React.Dispatch<React.SetStateAction<any | null>>;
|
||||
}
|
||||
|
||||
type TeamInterface = {
|
||||
models: any[];
|
||||
team_id: null;
|
||||
team_alias: string;
|
||||
max_budget: number | null;
|
||||
};
|
||||
|
||||
const DashboardTeam: React.FC<DashboardTeamProps> = ({
|
||||
teams,
|
||||
setSelectedTeam,
|
||||
userRole,
|
||||
proxySettings,
|
||||
setProxySettings,
|
||||
userInfo,
|
||||
accessToken,
|
||||
setKeys,
|
||||
}) => {
|
||||
console.log(`userInfo: ${JSON.stringify(userInfo)}`);
|
||||
const defaultTeam: TeamInterface = {
|
||||
models: userInfo?.models || [],
|
||||
team_id: null,
|
||||
team_alias: "Default Team",
|
||||
max_budget: userInfo?.max_budget || null,
|
||||
};
|
||||
|
||||
const getProxySettings = async () => {
|
||||
if (proxySettings === null && accessToken) {
|
||||
const proxy_settings: ProxySettings = await getProxyUISettings(accessToken);
|
||||
setProxySettings(proxy_settings);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getProxySettings();
|
||||
}, [proxySettings]);
|
||||
|
||||
const [value, setValue] = useState(defaultTeam);
|
||||
|
||||
let updatedTeams;
|
||||
console.log(`userRole: ${userRole}`);
|
||||
console.log(`proxySettings: ${JSON.stringify(proxySettings)}`);
|
||||
if (userRole === "App User") {
|
||||
// Non-Admin SSO users should only see their own team - they should not see "Default Team"
|
||||
updatedTeams = teams;
|
||||
} else if (proxySettings && proxySettings.DEFAULT_TEAM_DISABLED === true) {
|
||||
updatedTeams = teams ? [...teams] : [defaultTeam];
|
||||
} else {
|
||||
updatedTeams = teams ? [...teams, defaultTeam] : [defaultTeam];
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-5 mb-5">
|
||||
<Title>Select Team</Title>
|
||||
|
||||
<Text>
|
||||
If you belong to multiple teams, this setting controls which team is used by default when creating new Virtual
|
||||
Keys.
|
||||
</Text>
|
||||
<Text className="mt-3 mb-3">
|
||||
<b>Default Team:</b> If no team_id is set for a key, it will be grouped under here.
|
||||
</Text>
|
||||
|
||||
{updatedTeams && updatedTeams.length > 0 ? (
|
||||
<Select defaultValue="0">
|
||||
{updatedTeams.map((team: any, index) => (
|
||||
<SelectItem
|
||||
key={index}
|
||||
value={String(index)}
|
||||
onClick={() => {
|
||||
setSelectedTeam(team);
|
||||
// setKeys(team["keys"]);
|
||||
}}
|
||||
>
|
||||
{team["team_alias"]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
) : (
|
||||
<Text>
|
||||
No team created. <b>Defaulting to personal account.</b>
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardTeam;
|
||||
@@ -1,69 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, ChangeEvent } from "react";
|
||||
import { Button, Col, Grid, TextInput } from "@tremor/react";
|
||||
import { Card, Text } from "@tremor/react";
|
||||
|
||||
const EnterProxyUrl: React.FC = () => {
|
||||
const [proxyUrl, setProxyUrl] = useState<string>("");
|
||||
const [isUrlSaved, setIsUrlSaved] = useState<boolean>(false);
|
||||
|
||||
const handleUrlChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setProxyUrl(event.target.value);
|
||||
// Reset the saved status when the URL changes
|
||||
setIsUrlSaved(false);
|
||||
};
|
||||
|
||||
const handleSaveClick = () => {
|
||||
// You can perform any additional validation or actions here
|
||||
// For now, let's just display the message
|
||||
setIsUrlSaved(true);
|
||||
};
|
||||
|
||||
// Construct the URL for clicking
|
||||
const clickableUrl = `${window.location.href}?proxyBaseUrl=${proxyUrl}`;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card decoration="top" decorationColor="blue" style={{ width: "1000px" }}>
|
||||
<Text>Admin Configuration</Text>
|
||||
<label htmlFor="proxyUrl">Enter Proxy URL:</label>
|
||||
<TextInput
|
||||
type="text"
|
||||
id="proxyUrl"
|
||||
value={proxyUrl}
|
||||
onChange={handleUrlChange}
|
||||
placeholder="https://your-proxy-endpoint.com"
|
||||
/>
|
||||
<Button onClick={handleSaveClick} className="gap-2">
|
||||
Save
|
||||
</Button>
|
||||
{/* Display message if the URL is saved */}
|
||||
{isUrlSaved && (
|
||||
<div>
|
||||
<Grid numItems={1} className="gap-2">
|
||||
<Col>
|
||||
<p>Proxy Admin UI (Save this URL): {clickableUrl}</p>
|
||||
</Col>
|
||||
<Col>
|
||||
<p>
|
||||
Get Started with Proxy Admin UI 👉
|
||||
<a
|
||||
href={clickableUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ color: "blue", textDecoration: "underline" }}
|
||||
>
|
||||
{clickableUrl}
|
||||
</a>
|
||||
</p>
|
||||
</Col>
|
||||
</Grid>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnterProxyUrl;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Team } from "./key_list";
|
||||
|
||||
export const createTeamSearchFunction = (teams: Team[] | null) => {
|
||||
return async (searchText: string): Promise<Array<{ label: string; value: string }>> => {
|
||||
// Return empty array if teams is null or searchText is empty
|
||||
if (!teams || !searchText.trim()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Filter teams where team_alias contains the search text (case insensitive)
|
||||
const filteredTeams = teams.filter((team) => team.team_alias.toLowerCase().includes(searchText.toLowerCase()));
|
||||
|
||||
// Map filtered teams to the required format
|
||||
return filteredTeams.map((team) => ({
|
||||
label: `${team.team_alias} (${team.team_id.substring(0, 8)}...)`,
|
||||
value: team.team_id,
|
||||
}));
|
||||
};
|
||||
};
|
||||
@@ -1,279 +0,0 @@
|
||||
import React from "react";
|
||||
import { Typography, Space, Button, Divider } from "antd";
|
||||
import { WarningOutlined, InfoCircleOutlined, CopyOutlined } from "@ant-design/icons";
|
||||
import { testMCPConnectionRequest } from "./networking";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface MCPConnectionTestProps {
|
||||
formValues: Record<string, any>;
|
||||
accessToken: string;
|
||||
serverName?: string;
|
||||
onClose?: () => void;
|
||||
onTestComplete?: () => void;
|
||||
}
|
||||
|
||||
const MCPConnectionTest: React.FC<MCPConnectionTestProps> = ({
|
||||
formValues,
|
||||
accessToken,
|
||||
serverName = "this MCP server",
|
||||
onClose,
|
||||
onTestComplete,
|
||||
}) => {
|
||||
const [connectionError, setConnectionError] = React.useState<Error | string | null>(null);
|
||||
const [rawRequest, setRawRequest] = React.useState<any>(null);
|
||||
const [rawResponse, setRawResponse] = React.useState<any>(null);
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(true);
|
||||
const [connectionSuccess, setConnectionSuccess] = React.useState<boolean>(false);
|
||||
const [showDetails, setShowDetails] = React.useState<boolean>(false);
|
||||
|
||||
const testMCPConnection = async () => {
|
||||
setIsLoading(true);
|
||||
setShowDetails(false);
|
||||
setConnectionError(null);
|
||||
setRawRequest(null);
|
||||
setRawResponse(null);
|
||||
setConnectionSuccess(false);
|
||||
|
||||
// Add a small delay to ensure form values are fully populated
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
try {
|
||||
console.log("Testing MCP connection with form values:", formValues);
|
||||
|
||||
// Prepare the MCP server config from form values
|
||||
const mcpServerConfig = {
|
||||
server_id: formValues.server_id || "",
|
||||
alias: formValues.alias || "",
|
||||
url: formValues.url,
|
||||
transport: formValues.transport,
|
||||
auth_type: formValues.auth_type,
|
||||
mcp_info: formValues.mcp_info,
|
||||
};
|
||||
|
||||
setRawRequest(mcpServerConfig);
|
||||
|
||||
// Test connection
|
||||
const connectionResponse = await testMCPConnectionRequest(accessToken, mcpServerConfig);
|
||||
console.log("Connection test response:", connectionResponse);
|
||||
|
||||
if (connectionResponse.status === "ok") {
|
||||
setConnectionError(null);
|
||||
setConnectionSuccess(true);
|
||||
} else {
|
||||
const errorMessage = connectionResponse.message || "Unknown connection error";
|
||||
setConnectionError(errorMessage);
|
||||
setRawResponse(connectionResponse);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("MCP connection test error:", error);
|
||||
setConnectionError(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
if (onTestComplete) onTestComplete();
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
// Run the test once when component mounts
|
||||
// Add a small timeout to ensure form values are ready
|
||||
const timer = setTimeout(() => {
|
||||
testMCPConnection();
|
||||
}, 200);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []); // Empty dependency array means this runs once on mount
|
||||
|
||||
const getCleanErrorMessage = (errorMsg: string) => {
|
||||
if (!errorMsg) return "Unknown error";
|
||||
|
||||
const mainError = errorMsg.split("stack trace:")[0].trim();
|
||||
|
||||
const cleanedError = mainError.replace(/^(.*?)Error: /, "");
|
||||
|
||||
return cleanedError;
|
||||
};
|
||||
|
||||
const connectionErrorMessage =
|
||||
typeof connectionError === "string"
|
||||
? getCleanErrorMessage(connectionError)
|
||||
: connectionError?.message
|
||||
? getCleanErrorMessage(connectionError.message)
|
||||
: "Unknown error";
|
||||
|
||||
const formatMCPRequest = (mcpConfig: Record<string, any>) => {
|
||||
return JSON.stringify(mcpConfig, null, 2);
|
||||
};
|
||||
|
||||
const isOverallSuccess = connectionSuccess && !connectionError;
|
||||
|
||||
return (
|
||||
<div style={{ padding: "24px", borderRadius: "8px", backgroundColor: "#fff" }}>
|
||||
{isLoading ? (
|
||||
<div style={{ textAlign: "center", padding: "32px 20px" }}>
|
||||
<div className="loading-spinner" style={{ marginBottom: "16px" }}>
|
||||
{/* Simple CSS spinner */}
|
||||
<div
|
||||
style={{
|
||||
border: "3px solid #f3f3f3",
|
||||
borderTop: "3px solid #1890ff",
|
||||
borderRadius: "50%",
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
animation: "spin 1s linear infinite",
|
||||
margin: "0 auto",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Text style={{ fontSize: "16px" }}>Testing connection to {serverName}...</Text>
|
||||
<style jsx>{`
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
) : isOverallSuccess ? (
|
||||
<div>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", padding: "32px 20px" }}>
|
||||
<div style={{ color: "#52c41a", fontSize: "24px", display: "flex", alignItems: "center" }}>
|
||||
<svg
|
||||
viewBox="64 64 896 896"
|
||||
focusable="false"
|
||||
data-icon="check-circle"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<Text type="success" style={{ fontSize: "18px", fontWeight: 500, marginLeft: "10px" }}>
|
||||
Connection to {serverName} successful!
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<div style={{ display: "flex", alignItems: "center", marginBottom: "20px" }}>
|
||||
<WarningOutlined style={{ color: "#ff4d4f", fontSize: "24px", marginRight: "12px" }} />
|
||||
<Text type="danger" style={{ fontSize: "18px", fontWeight: 500 }}>
|
||||
Connection to {serverName} failed
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#fff2f0",
|
||||
border: "1px solid #ffccc7",
|
||||
borderRadius: "8px",
|
||||
padding: "16px",
|
||||
marginBottom: "20px",
|
||||
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.03)",
|
||||
}}
|
||||
>
|
||||
<Text strong style={{ display: "block", marginBottom: "8px" }}>
|
||||
Error:{" "}
|
||||
</Text>
|
||||
<Text type="danger" style={{ fontSize: "14px", lineHeight: "1.5" }}>
|
||||
{connectionErrorMessage}
|
||||
</Text>
|
||||
|
||||
{connectionError && (
|
||||
<div style={{ marginTop: "12px" }}>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
style={{ paddingLeft: 0, height: "auto" }}
|
||||
>
|
||||
{showDetails ? "Hide Details" : "Show Details"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showDetails && (
|
||||
<div style={{ marginBottom: "20px" }}>
|
||||
<Text strong style={{ display: "block", marginBottom: "8px", fontSize: "15px" }}>
|
||||
Troubleshooting Details
|
||||
</Text>
|
||||
<pre
|
||||
style={{
|
||||
backgroundColor: "#f5f5f5",
|
||||
padding: "16px",
|
||||
borderRadius: "8px",
|
||||
fontSize: "13px",
|
||||
maxHeight: "200px",
|
||||
overflow: "auto",
|
||||
border: "1px solid #e8e8e8",
|
||||
lineHeight: "1.5",
|
||||
}}
|
||||
>
|
||||
{typeof connectionError === "string" ? connectionError : JSON.stringify(connectionError, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Text strong style={{ display: "block", marginBottom: "8px", fontSize: "15px" }}>
|
||||
MCP Server Configuration
|
||||
</Text>
|
||||
<pre
|
||||
style={{
|
||||
backgroundColor: "#f5f5f5",
|
||||
padding: "16px",
|
||||
borderRadius: "8px",
|
||||
fontSize: "13px",
|
||||
maxHeight: "250px",
|
||||
overflow: "auto",
|
||||
border: "1px solid #e8e8e8",
|
||||
lineHeight: "1.5",
|
||||
}}
|
||||
>
|
||||
{formatMCPRequest(rawRequest || {})}
|
||||
</pre>
|
||||
<Button
|
||||
style={{ marginTop: "8px" }}
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(formatMCPRequest(rawRequest || {}));
|
||||
NotificationsManager.success("Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
Copy Configuration
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Divider style={{ margin: "24px 0 16px" }} />
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<Button
|
||||
type="link"
|
||||
href="https://docs.litellm.ai/docs/proxy/mcp_server"
|
||||
target="_blank"
|
||||
icon={<InfoCircleOutlined />}
|
||||
>
|
||||
View MCP Documentation
|
||||
</Button>
|
||||
<Space>
|
||||
<Button onClick={testMCPConnection} loading={isLoading}>
|
||||
Test Again
|
||||
</Button>
|
||||
<Button type="primary" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MCPConnectionTest;
|
||||
@@ -1,90 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { Modal } from "antd";
|
||||
import { Button as TremorButton } from "@tremor/react";
|
||||
import { ExclamationIcon } from "@heroicons/react/outline";
|
||||
|
||||
interface CredentialDeleteModalProps {
|
||||
isVisible: boolean;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
credentialName: string;
|
||||
}
|
||||
|
||||
const CredentialDeleteModal: React.FC<CredentialDeleteModalProps> = ({
|
||||
isVisible,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
credentialName,
|
||||
}) => {
|
||||
const [deleteConfirmInput, setDeleteConfirmInput] = useState("");
|
||||
const isValid = deleteConfirmInput === credentialName;
|
||||
|
||||
const handleCancel = () => {
|
||||
setDeleteConfirmInput("");
|
||||
onCancel();
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
if (isValid) {
|
||||
setDeleteConfirmInput("");
|
||||
onConfirm();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<ExclamationIcon className="h-6 w-6 text-red-600 mr-2" />
|
||||
Delete Credential
|
||||
</div>
|
||||
}
|
||||
open={isVisible}
|
||||
footer={null}
|
||||
onCancel={handleCancel}
|
||||
closable={true}
|
||||
destroyOnHidden={true}
|
||||
maskClosable={false}
|
||||
>
|
||||
<div className="mt-4">
|
||||
<div className="flex items-start gap-3 p-4 bg-red-50 border border-red-100 rounded-md mb-5">
|
||||
<div className="text-red-500 mt-0.5">
|
||||
<ExclamationIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-base font-medium text-red-600">
|
||||
This action cannot be undone and may break existing integrations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-5">
|
||||
<label className="block text-base font-medium text-gray-700 mb-2">
|
||||
{`Type `}
|
||||
<span className="underline italic">'{credentialName}'</span>
|
||||
{` to confirm deletion:`}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={deleteConfirmInput}
|
||||
onChange={(e) => setDeleteConfirmInput(e.target.value)}
|
||||
placeholder="Enter credential name exactly"
|
||||
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-base"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<TremorButton onClick={handleCancel} variant="secondary" className="mr-2">
|
||||
Cancel
|
||||
</TremorButton>
|
||||
<TremorButton onClick={handleConfirm} color="red" className="focus:ring-red-500" disabled={!isValid}>
|
||||
Delete Credential
|
||||
</TremorButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CredentialDeleteModal;
|
||||
@@ -83,7 +83,7 @@ const defaultServerRootPath = "/";
|
||||
export let serverRootPath = defaultServerRootPath;
|
||||
export let proxyBaseUrl = defaultProxyBaseUrl;
|
||||
if (isLocal != true) {
|
||||
console.log = function () {};
|
||||
console.log = function () { };
|
||||
}
|
||||
|
||||
const getWindowLocation = () => {
|
||||
@@ -136,8 +136,6 @@ const HTTP_REQUEST = {
|
||||
DELETE: "DELETE",
|
||||
};
|
||||
|
||||
export const DEFAULT_ORGANIZATION = "default_organization";
|
||||
|
||||
export interface Model {
|
||||
model_name: string;
|
||||
litellm_params: object;
|
||||
@@ -533,38 +531,6 @@ export const modelCreateCall = async (accessToken: string, formValues: Model) =>
|
||||
}
|
||||
};
|
||||
|
||||
export const modelSettingsCall = async (accessToken: string) => {
|
||||
/**
|
||||
* Get all configurable params for setting a model
|
||||
*/
|
||||
try {
|
||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/model/settings` : `/model/settings`;
|
||||
|
||||
//NotificationsManager.info("Requesting model data");
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
//NotificationsManager.info("Received model data");
|
||||
return data;
|
||||
// Handle success - you might want to update some state or UI based on the created key
|
||||
} catch (error: any) {
|
||||
console.error("Failed to get model settings:", error);
|
||||
}
|
||||
};
|
||||
|
||||
export const modelDeleteCall = async (accessToken: string, model_id: string) => {
|
||||
console.log(`model_id in model delete call: ${model_id}`);
|
||||
try {
|
||||
@@ -2301,178 +2267,6 @@ export const deleteAllowedIP = async (accessToken: string, ip: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const modelMetricsCall = async (
|
||||
accessToken: string,
|
||||
userID: string,
|
||||
userRole: string,
|
||||
modelGroup: string | null,
|
||||
startTime: string | undefined,
|
||||
endTime: string | undefined,
|
||||
apiKey: string | null,
|
||||
customer: string | null,
|
||||
) => {
|
||||
/**
|
||||
* Get all models on proxy
|
||||
*/
|
||||
try {
|
||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/model/metrics` : `/model/metrics`;
|
||||
if (modelGroup) {
|
||||
url = `${url}?_selected_model_group=${modelGroup}&startTime=${startTime}&endTime=${endTime}&api_key=${apiKey}&customer=${customer}`;
|
||||
}
|
||||
// NotificationsManager.info("Requesting model data");
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// NotificationsManager.info("Received model data");
|
||||
return data;
|
||||
// Handle success - you might want to update some state or UI based on the created key
|
||||
} catch (error) {
|
||||
console.error("Failed to create key:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
export const streamingModelMetricsCall = async (
|
||||
accessToken: string,
|
||||
modelGroup: string | null,
|
||||
startTime: string | undefined,
|
||||
endTime: string | undefined,
|
||||
) => {
|
||||
/**
|
||||
* Get all models on proxy
|
||||
*/
|
||||
try {
|
||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/model/streaming_metrics` : `/model/streaming_metrics`;
|
||||
if (modelGroup) {
|
||||
url = `${url}?_selected_model_group=${modelGroup}&startTime=${startTime}&endTime=${endTime}`;
|
||||
}
|
||||
// NotificationsManager.info("Requesting model data");
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// NotificationsManager.info("Received model data");
|
||||
return data;
|
||||
// Handle success - you might want to update some state or UI based on the created key
|
||||
} catch (error) {
|
||||
console.error("Failed to create key:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const modelMetricsSlowResponsesCall = async (
|
||||
accessToken: string,
|
||||
userID: string,
|
||||
userRole: string,
|
||||
modelGroup: string | null,
|
||||
startTime: string | undefined,
|
||||
endTime: string | undefined,
|
||||
apiKey: string | null,
|
||||
customer: string | null,
|
||||
) => {
|
||||
/**
|
||||
* Get all models on proxy
|
||||
*/
|
||||
try {
|
||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/model/metrics/slow_responses` : `/model/metrics/slow_responses`;
|
||||
if (modelGroup) {
|
||||
url = `${url}?_selected_model_group=${modelGroup}&startTime=${startTime}&endTime=${endTime}&api_key=${apiKey}&customer=${customer}`;
|
||||
}
|
||||
|
||||
// NotificationsManager.info("Requesting model data");
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// NotificationsManager.info("Received model data");
|
||||
return data;
|
||||
// Handle success - you might want to update some state or UI based on the created key
|
||||
} catch (error) {
|
||||
console.error("Failed to create key:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const modelExceptionsCall = async (
|
||||
accessToken: string,
|
||||
userID: string,
|
||||
userRole: string,
|
||||
modelGroup: string | null,
|
||||
startTime: string | undefined,
|
||||
endTime: string | undefined,
|
||||
apiKey: string | null,
|
||||
customer: string | null,
|
||||
) => {
|
||||
/**
|
||||
* Get all models on proxy
|
||||
*/
|
||||
try {
|
||||
let url = proxyBaseUrl ? `${proxyBaseUrl}/model/metrics/exceptions` : `/model/metrics/exceptions`;
|
||||
|
||||
if (modelGroup) {
|
||||
url = `${url}?_selected_model_group=${modelGroup}&startTime=${startTime}&endTime=${endTime}&api_key=${apiKey}&customer=${customer}`;
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
const errorMessage = deriveErrorMessage(errorData);
|
||||
handleError(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// NotificationsManager.info("Received model data");
|
||||
return data;
|
||||
// Handle success - you might want to update some state or UI based on the created key
|
||||
} catch (error) {
|
||||
console.error("Failed to create key:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateUsefulLinksCall = async (
|
||||
accessToken: string,
|
||||
useful_links: Record<string, string | { url: string; index: number }>,
|
||||
|
||||
@@ -1,61 +1,3 @@
|
||||
export interface Delta {
|
||||
content?: string;
|
||||
reasoning_content?: string;
|
||||
role?: string;
|
||||
function_call?: any;
|
||||
tool_calls?: any;
|
||||
audio?: any;
|
||||
refusal?: any;
|
||||
provider_specific_fields?: any;
|
||||
image?: {
|
||||
url: string;
|
||||
detail: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CompletionTokensDetails {
|
||||
accepted_prediction_tokens?: number;
|
||||
audio_tokens?: number;
|
||||
reasoning_tokens?: number;
|
||||
rejected_prediction_tokens?: number;
|
||||
text_tokens?: number | null;
|
||||
}
|
||||
|
||||
export interface PromptTokensDetails {
|
||||
audio_tokens?: number;
|
||||
cached_tokens?: number;
|
||||
text_tokens?: number;
|
||||
image_tokens?: number;
|
||||
}
|
||||
|
||||
export interface Usage {
|
||||
completion_tokens: number;
|
||||
prompt_tokens: number;
|
||||
total_tokens: number;
|
||||
completion_tokens_details?: CompletionTokensDetails;
|
||||
prompt_tokens_details?: PromptTokensDetails;
|
||||
}
|
||||
|
||||
export interface StreamingChoices {
|
||||
finish_reason?: string | null;
|
||||
index: number;
|
||||
delta: Delta;
|
||||
logprobs?: any;
|
||||
}
|
||||
|
||||
export interface StreamingResponse {
|
||||
id: string;
|
||||
created: number;
|
||||
model: string;
|
||||
object: string;
|
||||
system_fingerprint?: string;
|
||||
choices: StreamingChoices[];
|
||||
provider_specific_fields?: any;
|
||||
stream_options?: any;
|
||||
citations?: any;
|
||||
usage?: Usage;
|
||||
}
|
||||
|
||||
export interface VectorStoreSearchResult {
|
||||
score: number;
|
||||
content: Array<{ text: string; type: string }>;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,6 @@ export const FONT_SIZE_HEADER = 16;
|
||||
// Colors
|
||||
export const COLOR_BORDER = "#f0f0f0";
|
||||
export const COLOR_BACKGROUND = "#fff";
|
||||
export const COLOR_SECONDARY = "#8c8c8c";
|
||||
export const COLOR_BG_LIGHT = "#fafafa";
|
||||
|
||||
// Spacing
|
||||
@@ -38,5 +37,3 @@ export const SPACING_MEDIUM = 8;
|
||||
export const SPACING_LARGE = 12;
|
||||
export const SPACING_XLARGE = 16;
|
||||
export const SPACING_XXLARGE = 24;
|
||||
|
||||
// Messages (kept for backwards compatibility if needed elsewhere)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { getCountryFromIP } from "./ip_lookup";
|
||||
|
||||
interface CountryCellProps {
|
||||
ipAddress: string | null;
|
||||
}
|
||||
|
||||
export const CountryCell: React.FC<CountryCellProps> = ({ ipAddress }) => {
|
||||
const [country, setCountry] = React.useState<string>("-");
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!ipAddress) return;
|
||||
|
||||
let mounted = true;
|
||||
getCountryFromIP(ipAddress)
|
||||
.then((result) => {
|
||||
if (mounted) setCountry(result);
|
||||
})
|
||||
.catch(() => {
|
||||
if (mounted) setCountry("-");
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [ipAddress]);
|
||||
|
||||
return <span>{country}</span>;
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import { debounce } from "lodash";
|
||||
import { defaultPageSize } from "../constants";
|
||||
import { PaginatedResponse } from ".";
|
||||
|
||||
export const FILTER_KEYS = {
|
||||
const FILTER_KEYS = {
|
||||
TEAM_ID: "Team ID",
|
||||
KEY_HASH: "Key Hash",
|
||||
REQUEST_ID: "Request ID",
|
||||
|
||||
Reference in New Issue
Block a user