From e25f7f1f7a4020f8f3e289f3960ad9790a7f95d4 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Wed, 4 Feb 2026 18:19:33 -0800 Subject: [PATCH] refactor ui --- .../MCPSemanticFilterSettings.tsx | 268 +++++++++++------- .../MCPSemanticFilterTestPanel.tsx | 144 ++++++++++ .../semanticFilterTestUtils.ts | 95 +++++++ .../src/components/networking.tsx | 64 +++++ 4 files changed, 461 insertions(+), 110 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterTestPanel.tsx create mode 100644 ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/semanticFilterTestUtils.ts diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings.tsx index 4e2152c8482..6e669f74ab3 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings.tsx @@ -7,8 +7,10 @@ import { Alert, Button, Card, + Col, Form, InputNumber, + Row, Select, Skeleton, Slider, @@ -20,6 +22,8 @@ import { import { QuestionCircleOutlined, CheckCircleOutlined, SaveOutlined } from "@ant-design/icons"; import { useEffect, useState } from "react"; import { fetchAvailableModels, ModelGroup } from "@/components/playground/llm_calls/fetch_models"; +import MCPSemanticFilterTestPanel from "./MCPSemanticFilterTestPanel"; +import { getCurlCommand, runSemanticFilterTest, TestResult } from "./semanticFilterTestUtils"; interface MCPSemanticFilterSettingsProps { accessToken: string | null; @@ -37,6 +41,13 @@ export default function MCPSemanticFilterSettings({ accessToken }: MCPSemanticFi const [isDirty, setIsDirty] = useState(false); const [embeddingModels, setEmbeddingModels] = useState([]); const [loadingModels, setLoadingModels] = useState(true); + + // Test section state + const [testQuery, setTestQuery] = useState(""); + const [testModel, setTestModel] = useState("gpt-4o"); + const [testResult, setTestResult] = useState(null); + const [isTesting, setIsTesting] = useState(false); + const [showCurl, setShowCurl] = useState(false); const schema = data?.field_schema; const values = data?.values ?? {}; @@ -92,6 +103,20 @@ export default function MCPSemanticFilterSettings({ accessToken }: MCPSemanticFi } }; + const handleTest = async () => { + if (!accessToken) { + return; + } + + await runSemanticFilterTest({ + accessToken, + testModel, + testQuery, + setIsTesting, + setTestResult, + }); + }; + if (!accessToken) { return (
@@ -101,7 +126,7 @@ export default function MCPSemanticFilterSettings({ accessToken }: MCPSemanticFi } return ( -
+
{isLoading ? ( ) : isError ? ( @@ -143,120 +168,143 @@ export default function MCPSemanticFilterSettings({ accessToken }: MCPSemanticFi /> )} -
{ - setIsDirty(true); - }} - > - - - Enable Semantic Filtering - - - - - } - valuePropName="checked" + + {/* Left Column - Settings */} + + { + setIsDirty(true); + }} > - - - - - {schema?.properties?.enabled?.description} - - + + + Enable Semantic Filtering + + + + + } + valuePropName="checked" + > + + + + + {schema?.properties?.enabled?.description} + + - - - Embedding Model - - - - - } - > - ({ + label: model.model_group, + value: model.model_group, + }))} + placeholder={loadingModels ? "Loading models..." : "Select embedding model"} + showSearch + disabled={isUpdating || loadingModels} + loading={loadingModels} + notFoundContent={ + loadingModels ? "Loading..." : "No embedding models available" + } + /> + - - Top K Results - - - - - } - > - - + + Top K Results + + + + + } + > + + - - Similarity Threshold - - - - - } - > - - - + + Similarity Threshold + + + + + } + > + + + -
- -
-
+
+ +
+ + + + {/* Right Column - Test Configuration */} + + + + )}
diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterTestPanel.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterTestPanel.tsx new file mode 100644 index 00000000000..c2746aac511 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterTestPanel.tsx @@ -0,0 +1,144 @@ +import { CodeOutlined, PlayCircleOutlined } from "@ant-design/icons"; +import { Alert, Button, Card, Input, Space, Typography } from "antd"; +import ModelSelector from "@/components/common_components/ModelSelector"; +import { TestResult } from "./semanticFilterTestUtils"; + +interface MCPSemanticFilterTestPanelProps { + accessToken: string | null; + testQuery: string; + setTestQuery: (value: string) => void; + testModel: string; + setTestModel: (value: string) => void; + isTesting: boolean; + onTest: () => void; + filterEnabled: boolean; + testResult: TestResult | null; + showCurl: boolean; + setShowCurl: (value: boolean) => void; + curlCommand: string; +} + +export default function MCPSemanticFilterTestPanel({ + accessToken, + testQuery, + setTestQuery, + testModel, + setTestModel, + isTesting, + onTest, + filterEnabled, + testResult, + showCurl, + setShowCurl, + curlCommand, +}: MCPSemanticFilterTestPanelProps) { + return ( + <> + + +
+ + Test Query + + setTestQuery(e.target.value)} + rows={4} + disabled={isTesting} + /> +
+ +
+ +
+ + + + {!filterEnabled && ( + + )} + + {testResult && ( +
+ Results + +
+ + Selected Tools: + +
    + {testResult.tools.map((tool, index) => ( +
  • + {tool} +
  • + ))} +
+
+
+ )} +
+
+ + + + API Usage + + } + > + + Use this curl command to test the semantic filter with your current configuration. + + + {showCurl && ( +
+            {curlCommand}
+          
+ )} +
+ + ); +} diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/semanticFilterTestUtils.ts b/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/semanticFilterTestUtils.ts new file mode 100644 index 00000000000..e80ed6b08d7 --- /dev/null +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/semanticFilterTestUtils.ts @@ -0,0 +1,95 @@ +import NotificationManager from "@/components/molecules/notifications_manager"; +import { testMCPSemanticFilter } from "@/components/networking"; + +export interface TestResult { + totalTools: number; + selectedTools: number; + tools: string[]; +} + +interface FilterHeaders { + filter: string | null; + tools: string | null; +} + +const parseFilterHeaders = (headers: FilterHeaders): TestResult | null => { + if (!headers.filter) { + return null; + } + + const [total, selected] = headers.filter.split("->").map(Number); + const tools = headers.tools + ? headers.tools.split(",").map((name) => name.trim()) + : []; + + return { totalTools: total, selectedTools: selected, tools }; +}; + +export const runSemanticFilterTest = async ({ + accessToken, + testModel, + testQuery, + setIsTesting, + setTestResult, +}: { + accessToken: string; + testModel: string; + testQuery: string; + setIsTesting: (value: boolean) => void; + setTestResult: (result: TestResult | null) => void; +}) => { + if (!testQuery || !testModel || !accessToken) { + NotificationManager.error("Please enter a query and select a model"); + return; + } + + setIsTesting(true); + setTestResult(null); + + try { + const { headers } = await testMCPSemanticFilter( + accessToken, + testModel, + testQuery + ); + const parsedResult = parseFilterHeaders(headers); + + if (!parsedResult) { + NotificationManager.warning( + "Semantic filter is not enabled or no tools were filtered" + ); + return; + } + + setTestResult(parsedResult); + NotificationManager.success("Semantic filter test completed successfully"); + } catch (error) { + console.error("Test failed:", error); + NotificationManager.error("Failed to test semantic filter"); + } finally { + setIsTesting(false); + } +}; + +export const getCurlCommand = (testModel: string, testQuery: string) => + `curl --location 'http://localhost:4000/v1/responses' \\ +--header 'Content-Type: application/json' \\ +--header 'Authorization: Bearer sk-1234' \\ +--data '{ + "model": "${testModel}", + "input": [ + { + "role": "user", + "content": "${testQuery || "Your query here"}", + "type": "message" + } + ], + "tools": [ + { + "type": "mcp", + "server_url": "litellm_proxy", + "require_approval": "never" + } + ], + "tool_choice": "required" +}'`; diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 84b6de5b7b4..84895c64ff4 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -5465,6 +5465,70 @@ export const updateMCPSemanticFilterSettings = async ( } }; +export const testMCPSemanticFilter = async ( + accessToken: string, + model: string, + query: string +) => { + /** + * Test MCP semantic filter by making a responses API call + * Returns both the response data and headers containing filter information + */ + try { + const url = proxyBaseUrl ? `${proxyBaseUrl}/v1/responses` : `/v1/responses`; + const response = await fetch(url, { + method: "POST", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: model, + input: [ + { + role: "user", + content: query, + type: "message", + }, + ], + tools: [ + { + type: "mcp", + server_url: "litellm_proxy", + require_approval: "never", + }, + ], + tool_choice: "required", + }), + }); + + // Extract headers before checking response status + const filterHeader = response.headers.get("x-litellm-semantic-filter"); + const toolsHeader = response.headers.get("x-litellm-semantic-filter-tools"); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + const data = await response.json(); + + // Return both data and headers + return { + data, + headers: { + filter: filterHeader, + tools: toolsHeader, + }, + }; + } catch (error) { + console.error("Failed to test MCP semantic filter:", error); + throw error; + } +}; + export const getGuardrailsList = async (accessToken: string) => { try { const url = proxyBaseUrl ? `${proxyBaseUrl}/v2/guardrails/list` : `/v2/guardrails/list`;