diff --git a/tests/e2e/ui/tests/proxy-admin/teams.spec.ts b/tests/e2e/ui/tests/proxy-admin/teams.spec.ts index 92d22f11f4d..3a63c5be940 100644 --- a/tests/e2e/ui/tests/proxy-admin/teams.spec.ts +++ b/tests/e2e/ui/tests/proxy-admin/teams.spec.ts @@ -47,10 +47,10 @@ test.describe("Proxy Admin - Teams", () => { // Fill Team Name — the input has id="team_alias" await dialog.locator("#team_alias").fill(uniqueAlias); - // Select models — the models multi-select is inside the modal - // Click to open dropdown, select "All Proxy Models" - await dialog.locator(".ant-select-selection-overflow").first().click(); - await page.locator(".ant-select-dropdown:visible").getByText("All Proxy Models").click(); + // Select models — the models multi-select is inside the modal. Its popup is + // portaled to the body, so scope the option lookup to the page, not the dialog. + await dialog.getByTestId("create-team-models-select").getByRole("combobox").click(); + await page.getByRole("option", { name: "All Proxy Models", exact: true }).click(); await page.keyboard.press("Escape"); // Submit — click the submit button inside the dialog (not the header button) @@ -191,11 +191,11 @@ test.describe("Proxy Admin - Teams", () => { const modelsSelect = page.locator("[data-testid='models-select']"); await expect(modelsSelect).toBeVisible({ timeout: 10_000 }); - const anthropicTag = modelsSelect - .locator(".ant-select-selection-item") + const anthropicChip = modelsSelect + .locator('[data-slot="combobox-chip"]') .filter({ hasText: "fake-anthropic-claude" }); - await expect(anthropicTag).toBeVisible({ timeout: 5_000 }); - await anthropicTag.locator(".ant-select-selection-item-remove").click(); + await expect(anthropicChip).toBeVisible({ timeout: 5_000 }); + await anthropicChip.locator('[data-slot="combobox-chip-remove"]').click(); await page.getByRole("button", { name: "Save Changes" }).click(); diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 5e322598a10..eb3352a0e30 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1830,9 +1830,6 @@ "src/components/GuardrailsMonitor/LogViewer.tsx": { "no-nested-ternary": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 } }, "src/components/HelpLink.test.tsx": { @@ -1845,11 +1842,6 @@ "count": 1 } }, - "src/components/ModelSelect/ModelSelect.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/components/Navbar/BlogDropdown/BlogDropdown.test.tsx": { "max-nested-callbacks": { "count": 12 @@ -2341,9 +2333,6 @@ } }, "src/components/claude_code_plugins/MakeSkillPublicForm.tsx": { - "no-restricted-imports": { - "count": 2 - }, "react-hooks/set-state-in-effect": { "count": 1 } @@ -2982,9 +2971,6 @@ }, "max-lines": { "count": 1 - }, - "no-restricted-imports": { - "count": 2 } }, "src/components/query_param_input.tsx": { diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx b/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx index c3671fc9e2c..d41b29f8218 100644 --- a/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx +++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/LogViewer.tsx @@ -1,8 +1,9 @@ -import { CheckCircleOutlined, CloseOutlined, DownOutlined, WarningOutlined } from "@ant-design/icons"; +import { CircleCheck, ChevronDown, TriangleAlert, X } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import moment from "moment"; -import { Button, Spin } from "antd"; import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { uiSpendLogsCall } from "@/components/networking"; import { LogDetailsDrawer } from "@/components/view_logs/LogDetailsDrawer"; import type { LogEntry as ViewLogsLogEntry } from "@/components/view_logs/columns"; @@ -13,21 +14,21 @@ const actionConfig: Record< { icon: React.ElementType; color: string; bg: string; border: string; label: string } > = { blocked: { - icon: CloseOutlined, + icon: X, color: "text-red-600", bg: "bg-red-50", border: "border-red-200", label: "Blocked", }, passed: { - icon: CheckCircleOutlined, + icon: CircleCheck, color: "text-green-600", bg: "bg-green-50", border: "border-green-200", label: "Passed", }, flagged: { - icon: WarningOutlined, + icon: TriangleAlert, color: "text-amber-600", bg: "bg-amber-50", border: "border-amber-200", @@ -125,8 +126,8 @@ export function LogViewer({ {filters.map((f) => ( ); })} diff --git a/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.test.tsx b/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.test.tsx index e253bc4c0ef..eeaeac541bd 100644 --- a/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.test.tsx +++ b/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.test.tsx @@ -1,6 +1,6 @@ import type { ProxyModel } from "@/app/(dashboard)/hooks/models/useModels"; import type { Organization } from "@/components/networking"; -import { screen, waitFor } from "@testing-library/react"; +import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "../../../tests/test-utils"; @@ -22,64 +22,6 @@ vi.mock("@/app/(dashboard)/hooks/users/useCurrentUser", () => ({ useCurrentUser: vi.fn(), })); -vi.mock("antd", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - Select: ({ - value, - onChange, - options, - "data-testid": dataTestId, - allowClear, - maxTagCount, - maxTagPlaceholder, - mode, - ...props - }: any) => { - // Simulate maxTagCount responsive behavior - if value length > 5, call maxTagPlaceholder - const shouldShowPlaceholder = maxTagCount === "responsive" && Array.isArray(value) && value.length > 5; - const visibleValues = shouldShowPlaceholder ? value.slice(0, 5) : value; - const omittedValues = shouldShowPlaceholder ? value.slice(5).map((v: string) => ({ value: v, label: v })) : []; - - return ( -
- - {shouldShowPlaceholder && maxTagPlaceholder && ( -
{maxTagPlaceholder(omittedValues)}
- )} -
- ); - }, - Skeleton: { - Input: ({ active, block }: any) =>
, - }, - Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}, - }; -}); - import { useAllProxyModels } from "@/app/(dashboard)/hooks/models/useModels"; import { useOrganization } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; import { useTeam } from "@/app/(dashboard)/hooks/teams/useTeams"; @@ -108,6 +50,14 @@ const createMockOrganization = (models: string[]): Organization => ({ members: null, }); +const openModelList = async (user: ReturnType) => { + await user.click(screen.getAllByRole("combobox")[0]); + await screen.findByRole("listbox"); +}; + +const expectOffered = (label: string) => expect(screen.queryAllByText(label).length).toBeGreaterThan(0); +const expectNotOffered = (label: string) => expect(screen.queryAllByText(label)).toHaveLength(0); + describe("ModelSelect", () => { const mockProxyModels: ProxyModel[] = [ { id: "gpt-4", object: "model", created: 1234567890, owned_by: "openai" }, @@ -138,21 +88,26 @@ describe("ModelSelect", () => { } as any); }); - it("should render with all option groups", async () => { + it("should offer every model and wildcard under its group heading", async () => { + const user = userEvent.setup(); renderWithProviders( , ); - await waitFor(() => { - expect(screen.getByTestId("model-select")).toBeInTheDocument(); - expect(screen.getByText("gpt-4")).toBeInTheDocument(); - expect(screen.getByText("claude-3")).toBeInTheDocument(); - expect(screen.getByText("All Openai models")).toBeInTheDocument(); - expect(screen.getByText("All Anthropic models")).toBeInTheDocument(); - }); + await openModelList(user); + + expectOffered("Wildcard Options"); + expectOffered("gpt-4"); + expectOffered("claude-3"); + expectOffered("All Openai models"); + expectOffered("All Anthropic models"); }); - it("should show skeleton loader when any data is loading", () => { + it("should offer nothing to select while any dependency is loading", () => { + const { unmount: unmountReady } = renderWithProviders(); + expect(screen.getAllByRole("combobox")).toHaveLength(1); + unmountReady(); + const loadingScenarios = [ { hook: mockUseAllProxyModels, context: "user" as const }, { hook: mockUseTeam, context: "team" as const, props: { teamID: "team-1" } }, @@ -168,30 +123,24 @@ describe("ModelSelect", () => { const { unmount } = renderWithProviders(); - expect(screen.getByTestId("skeleton-input")).toBeInTheDocument(); + expect(screen.queryAllByRole("combobox")).toHaveLength(0); unmount(); }); }); - it("should handle model selection and onChange", async () => { + it("should report the picked model to onChange", async () => { const user = userEvent.setup(); renderWithProviders( , ); - await waitFor(() => { - expect(screen.getByTestId("model-select")).toBeInTheDocument(); - }); + await openModelList(user); + await user.click(screen.getAllByText("gpt-4")[0]); - const select = screen.getByRole("listbox"); - await user.selectOptions(select, "gpt-4"); expect(mockOnChange).toHaveBeenCalledWith(["gpt-4"]); - - await user.selectOptions(select, ["gpt-4", "claude-3"]); - expect(mockOnChange).toHaveBeenCalled(); }); - it("should handle special options correctly", async () => { + it("should offer both special options when they are enabled", async () => { const user = userEvent.setup(); mockUseOrganization.mockReturnValue({ data: createMockOrganization(["all-proxy-models"]), @@ -207,33 +156,32 @@ describe("ModelSelect", () => { />, ); - await waitFor(() => { - expect(screen.getByText("All Proxy Models")).toBeInTheDocument(); - expect(screen.getByText("No Default Models")).toBeInTheDocument(); - }); + await openModelList(user); - const select = screen.getByRole("listbox"); - await user.selectOptions(select, ["all-proxy-models", "no-default-models"]); - expect(mockOnChange).toHaveBeenCalledWith(["no-default-models"]); + expectOffered("Special Options"); + expectOffered("All Proxy Models"); + expectOffered("No Default Models"); }); - it("should disable models when special option is selected", async () => { + it("should replace an existing selection when a special option is picked", async () => { + const user = userEvent.setup(); + renderWithProviders( , ); - await waitFor(() => { - expect(screen.getByRole("option", { name: "gpt-4" })).toBeDisabled(); - expect(screen.getByRole("option", { name: "All Openai models" })).toBeDisabled(); - }); + await openModelList(user); + await user.click(screen.getAllByText("No Default Models")[0]); + + expect(mockOnChange).toHaveBeenCalledWith(["no-default-models"]); }); - it("should filter models based on context", async () => { + it("should filter the offered models by context", async () => { const testCases = [ { name: "user context with includeUserModels", @@ -340,6 +288,7 @@ describe("ModelSelect", () => { ]; for (const testCase of testCases) { + const user = userEvent.setup(); testCase.setup(); const { unmount } = renderWithProviders( { />, ); - await waitFor(() => { - testCase.expectedVisible.forEach((model) => { - expect(screen.getByText(model)).toBeInTheDocument(); - }); - testCase.expectedHidden.forEach((model) => { - expect(screen.queryByText(model)).not.toBeInTheDocument(); - }); - }); + await openModelList(user); + testCase.expectedVisible.forEach(expectOffered); + testCase.expectedHidden.forEach(expectNotOffered); unmount(); vi.clearAllMocks(); @@ -368,7 +312,7 @@ describe("ModelSelect", () => { } }); - it("should show All Proxy Models option based on conditions", async () => { + it("should offer All Proxy Models only when the context allows it", async () => { const testCases = [ { name: "when showAllProxyModelsOverride is true", @@ -426,6 +370,7 @@ describe("ModelSelect", () => { ]; for (const testCase of testCases) { + const user = userEvent.setup(); testCase.setup(); const { unmount } = renderWithProviders( { />, ); - await waitFor(() => { - if (testCase.shouldShow) { - expect(screen.getByText("All Proxy Models")).toBeInTheDocument(); - } else { - expect(screen.queryByText("All Proxy Models")).not.toBeInTheDocument(); - expect(screen.getByText("No Default Models")).toBeInTheDocument(); - } - }); + await openModelList(user); + if (testCase.shouldShow) { + expectOffered("All Proxy Models"); + } else { + expectNotOffered("All Proxy Models"); + expectOffered("No Default Models"); + } unmount(); vi.clearAllMocks(); @@ -454,27 +398,6 @@ describe("ModelSelect", () => { } }); - it("should deduplicate models with same id", async () => { - const duplicateModels: ProxyModel[] = [ - { id: "gpt-4", object: "model", created: 1234567890, owned_by: "openai" }, - { id: "gpt-4", object: "model", created: 1234567890, owned_by: "openai" }, - ]; - - mockUseAllProxyModels.mockReturnValue({ - data: { data: duplicateModels }, - isLoading: false, - } as any); - - renderWithProviders( - , - ); - - await waitFor(() => { - const gpt4Options = screen.getAllByText("gpt-4"); - expect(gpt4Options.length).toBeGreaterThan(0); - }); - }); - it("should use custom dataTestId when provided", async () => { renderWithProviders( { />, ); - await waitFor(() => { - expect(screen.getByTestId("custom-test-id")).toBeInTheDocument(); - }); + expect(await screen.findByTestId("custom-test-id")).toBeInTheDocument(); }); it("should return all proxy models for team context when organization has empty models array", async () => { + const user = userEvent.setup(); mockUseTeam.mockReturnValue({ data: { team_id: "team-1", team_alias: "Test Team", models: [] }, isLoading: false, @@ -503,52 +425,66 @@ describe("ModelSelect", () => { renderWithProviders(); - await waitFor(() => { - expect(screen.getByText("gpt-4")).toBeInTheDocument(); - expect(screen.getByText("claude-3")).toBeInTheDocument(); - }); + await openModelList(user); + + expectOffered("gpt-4"); + expectOffered("claude-3"); }); - it("should disable No Default Models when all-proxy-models is selected", async () => { - mockUseOrganization.mockReturnValue({ - data: createMockOrganization(["all-proxy-models"]), - isLoading: false, - } as any); + it("should not offer a special options group when includeSpecialOptions is omitted", async () => { + const user = userEvent.setup(); + renderWithProviders(); + await openModelList(user); + + expectNotOffered("Special Options"); + expectNotOffered("All Proxy Models"); + expectNotOffered("No Default Models"); + expectOffered("Models"); + }); + + it("should mark models and wildcards unselectable while a special option is selected", async () => { + const user = userEvent.setup(); renderWithProviders( , ); - await waitFor(() => { - const noDefaultOption = screen.getByRole("option", { name: "No Default Models" }); - expect(noDefaultOption).toBeDisabled(); - }); + await openModelList(user); + + expect(screen.getByRole("option", { name: "gpt-4" })).toHaveAttribute("aria-disabled", "true"); + expect(screen.getByRole("option", { name: "All Openai models" })).toHaveAttribute("aria-disabled", "true"); + expect(screen.getByRole("option", { name: "No Default Models" })).toHaveAttribute("aria-disabled", "true"); + expect(screen.getByRole("option", { name: "All Proxy Models" })).not.toHaveAttribute("aria-disabled", "true"); }); - it("should not render an empty optgroup when includeSpecialOptions is omitted", async () => { - renderWithProviders(); + it("should list a duplicated proxy model only once", async () => { + const user = userEvent.setup(); + mockUseAllProxyModels.mockReturnValue({ + data: { + data: [ + { id: "gpt-4", object: "model", created: 1234567890, owned_by: "openai" }, + { id: "gpt-4", object: "model", created: 1234567890, owned_by: "openai" }, + ], + }, + isLoading: false, + } as any); - await waitFor(() => { - expect(screen.getByTestId("model-select")).toBeInTheDocument(); - }); + renderWithProviders( + , + ); - const optgroups = document.querySelectorAll("optgroup"); - // Wildcard Options + Models — no blank leading group - expect(optgroups.length).toBe(2); - optgroups.forEach((g) => { - expect(g.getAttribute("label")).toBeTruthy(); - }); + await openModelList(user); + + expect(screen.getAllByRole("option", { name: "gpt-4" })).toHaveLength(1); }); - it("should render maxTagPlaceholder when many items are selected", async () => { - // Create many models to trigger maxTagCount responsive behavior - const manyModels: ProxyModel[] = Array.from({ length: 20 }, (_, i) => ({ + it("should collapse selections past the chip limit into a labelled overflow count", async () => { + const manyModels: ProxyModel[] = Array.from({ length: 8 }, (_, i) => ({ id: `model-${i}`, object: "model", created: 1234567890, @@ -560,22 +496,18 @@ describe("ModelSelect", () => { isLoading: false, } as any); - const selectedValues = manyModels.slice(0, 10).map((m) => m.id); - renderWithProviders( m.id)} context="user" options={{ showAllProxyModelsOverride: true }} />, ); - await waitFor(() => { - expect(screen.getByTestId("model-select")).toBeInTheDocument(); - // Verify maxTagPlaceholder is rendered with omitted values - expect(screen.getByTestId("max-tag-placeholder")).toBeInTheDocument(); - expect(screen.getByText(/\+5 more/)).toBeInTheDocument(); - }); + expect(await screen.findByText("+3 more")).toBeInTheDocument(); + expect(screen.getByLabelText("model-0")).toBeInTheDocument(); + expect(screen.getByLabelText("model-4")).toBeInTheDocument(); + expect(screen.queryByLabelText("model-5")).not.toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.tsx b/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.tsx index e993fed2408..55aa1f1ec5f 100644 --- a/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.tsx +++ b/ui/litellm-dashboard/src/components/ModelSelect/ModelSelect.tsx @@ -2,7 +2,22 @@ import { ProxyModel, useAllProxyModels } from "@/app/(dashboard)/hooks/models/us import { useOrganization } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; import { useTeam } from "@/app/(dashboard)/hooks/teams/useTeams"; import { useCurrentUser } from "@/app/(dashboard)/hooks/users/useCurrentUser"; -import { Select, Skeleton, Tooltip } from "antd"; +import { + Combobox, + ComboboxChip, + ComboboxChips, + ComboboxChipsInput, + ComboboxCollection, + ComboboxContent, + ComboboxEmpty, + ComboboxGroup, + ComboboxItem, + ComboboxLabel, + ComboboxList, + ComboboxValue, +} from "@/components/ui/combobox"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { Organization, Team } from "../networking"; import { splitWildcardModels } from "./modelUtils"; @@ -21,6 +36,8 @@ export const MODEL_SENTINEL_OPTIONS = [ MODEL_SELECT_NO_DEFAULT_MODELS_SPECIAL_VALUE, ] as const; +const MAX_VISIBLE_MODEL_CHIPS = 5; + export interface ModelSelectProps { teamID?: string; organizationID?: string; @@ -37,6 +54,17 @@ export interface ModelSelectProps { style?: React.CSSProperties; } +type ModelOption = { + label: string; + value: string; + disabled?: boolean; +}; + +type ModelOptionGroup = { + label: string; + items: ModelOption[]; +}; + type FilterContextArgs = { allProxyModels: string[]; selectedTeam?: Team; @@ -109,10 +137,11 @@ export const ModelSelect = (props: ModelSelectProps) => { showAllProxyModelsOverride || (organizationHasAllProxyModels && includeSpecialOptions) || context === "global"; if (isLoading) { - return ; + return ; } - const handleChange = (values: string[]) => { + const handleChange = (selected: ModelOption[]) => { + const values = selected.map((option) => option.value); const specialValues = values.filter(isSpecialOption); let finalValues: string[]; @@ -133,85 +162,122 @@ export const ModelSelect = (props: ModelSelectProps) => { }); const { wildcard, regular } = splitWildcardModels(filteredModels); - return ( - setSearchTerm(e.target.value)} - className="border border-gray-300 rounded-lg pl-10 pr-4 py-2 w-full text-sm focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white" - /> -
- -
- Provider: - -
-
- Mode: - -
-
- Features: - -
- - - model.model_group || String(index)} - sortingMode="client" - sorting={modelSorting} - onSortingChange={setModelSorting} - isLoading={loading} - loadingMessage="Loading models…" - noDataMessage={ - - } - size="compact" - /> - -
- - Showing {filteredData.length} of {modelHubData?.length || 0} models - -
- - - {/* Agents Tab */} - {agentHubData && Array.isArray(agentHubData) && agentHubData.length > 0 && ( - -
- Available Agents -
- - {/* Filters */} -
-
-
- Search Agents: - - - -
-
- - setAgentSearchTerm(e.target.value)} - className="border border-gray-300 rounded-lg pl-10 pr-4 py-2 w-full text-sm focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white" - /> -
-
-
- Skills: - -
-
- - agent.name || String(index)} - sortingMode="client" - sorting={agentSorting} - onSortingChange={setAgentSorting} - isLoading={agentLoading} - loadingMessage="Loading agents…" - noDataMessage={ - - } - size="compact" - /> - -
- - Showing {filteredAgentData.length} of {agentHubData?.length || 0} agents - -
-
- )} - - {/* MCP Servers Tab */} - {mcpHubData && Array.isArray(mcpHubData) && mcpHubData.length > 0 && ( - -
- Available MCP Servers -
- - {/* Filters */} -
-
-
- Search MCP Servers: - - - -
-
- - setMcpSearchTerm(e.target.value)} - className="border border-gray-300 rounded-lg pl-10 pr-4 py-2 w-full text-sm focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white" - /> -
-
-
- Transport: - -
-
- - server.server_id || String(index)} - sortingMode="client" - sorting={mcpSorting} - onSortingChange={setMcpSorting} - isLoading={mcpLoading} - loadingMessage="Loading MCP servers…" - noDataMessage={ - - } - size="compact" - /> - -
- - Showing {filteredMcpData.length} of {mcpHubData?.length || 0} MCP servers - -
-
- )} - - {/* Skill Hub Tab */} - - - - - - - - {/* Model Details Modal */} - - {selectedModel?.model_group || "Model Details"} - {selectedModel && ( - - copyToClipboard(selectedModel.model_group)} - className="cursor-pointer text-gray-500 hover:text-blue-500 w-4 h-4" - /> - - )} - - } - width={1000} - open={isModalVisible} - footer={null} - onOk={handleModalOk} - onCancel={handleModalCancel} - > - {selectedModel && ( -
- {/* Model Overview */} -
- Model Overview -
-
- Model Name: - {selectedModel.model_group} -
-
- Mode: - {selectedModel.mode || "Not specified"} -
-
- Providers: -
- {(selectedModel.providers ?? []).map((provider) => { - const { logo } = getProviderLogoAndName(provider); - return ( - -
- {logo && ( - {provider} { - (e.target as HTMLImageElement).style.display = "none"; - }} - /> - )} - {provider} -
-
- ); - })} -
-
-
- - {/* Wildcard Routing Note */} - {selectedModel.model_group.includes("*") && ( -
-
- -
- Wildcard Routing - - This model uses wildcard routing. You can pass any value where you see the{" "} - * symbol. - - - For example, with{" "} - - {selectedModel.model_group} - - , you can use any string ( - - {selectedModel.model_group.replaceAll("*", "my-custom-value")} - - ) that matches this pattern. - -
-
-
- )} -
- - {/* Token and Cost Information */} -
- Token & Cost Information -
-
- Max Input Tokens: - {selectedModel.max_input_tokens?.toLocaleString() || "Not specified"} -
-
- Max Output Tokens: - {selectedModel.max_output_tokens?.toLocaleString() || "Not specified"} -
-
- Input Cost per 1M Tokens: - - {selectedModel.input_cost_per_token - ? formatCost(selectedModel.input_cost_per_token) - : "Not specified"} - -
-
- Output Cost per 1M Tokens: - - {selectedModel.output_cost_per_token - ? formatCost(selectedModel.output_cost_per_token) - : "Not specified"} - -
-
-
- - {/* Capabilities */} -
- Capabilities -
- {(() => { - const capabilities = getModelCapabilities(selectedModel); - const colors = ["green", "blue", "purple", "orange", "red", "yellow"]; - - if (capabilities.length === 0) { - return No special capabilities listed; - } - - return capabilities.map((capability, index) => ( - - {formatCapabilityName(capability)} - - )); - })()} -
-
- - {/* Rate Limits */} - {(selectedModel.tpm || selectedModel.rpm) && ( -
- Rate Limits -
- {selectedModel.tpm && ( -
- Tokens per Minute: - {selectedModel.tpm.toLocaleString()} -
- )} - {selectedModel.rpm && ( -
- Requests per Minute: - {selectedModel.rpm.toLocaleString()} -
- )} -
-
- )} - - {/* Supported OpenAI Parameters */} - {selectedModel.supported_openai_params && selectedModel.supported_openai_params.length > 0 && ( -
- Supported OpenAI Parameters -
- {selectedModel.supported_openai_params.map((param) => ( - - {param} - + +

{title}

+ ))} -
- )} + + )} - {/* Usage Example */} -
- Usage Example -
-
-                    {(() => {
-                      const codeSnippet = generateCodeSnippet({
-                        apiKeySource: "custom",
-                        accessToken: null,
-                        apiKey: "your_api_key",
-                        inputMessage: "Hello, how are you?",
-                        chatHistory: [{ role: "user", content: "Hello, how are you?", isImage: false } as MessageType],
-                        selectedTags: [],
-                        selectedVectorStores: [],
-                        selectedGuardrails: [],
-                        selectedPolicies: [],
-                        selectedMCPServers: [],
-                        endpointType: getEndpointType(selectedModel.mode || "chat"),
-                        selectedModel: selectedModel.model_group,
-                        selectedSdk: "openai",
-                      });
-                      return codeSnippet;
-                    })()}
-                  
+ {/* Health and Endpoint Status - only shown when not embedded */} + {!isEmbedded && ( + +

Health and Endpoint Status

+
+

Service status: {serviceStatus}

-
- -
-
-
- )} - + + )} - {/* Agent Details Modal */} - - {selectedAgent?.name || "Agent Details"} - {selectedAgent && ( - - copyToClipboard(selectedAgent.name)} - className="cursor-pointer text-gray-500 hover:text-blue-500 w-4 h-4" - /> - - )} -
- } - width={1000} - open={isAgentModalVisible} - footer={null} - onOk={handleAgentModalOk} - onCancel={handleAgentModalCancel} - > - {selectedAgent && ( -
- {/* Agent Overview */} -
- Agent Overview -
-
- Name: - {selectedAgent.name} + {/* Tabs for Models and Agents */} + + + + Model Hub + {hasAgents && Agent Hub} + {hasMcpServers && MCP Hub} + Skill Hub + + + {/* Models Tab */} + +
+

Available Models

-
- Version: - {selectedAgent.version} -
-
- Description: - {selectedAgent.description} -
- {selectedAgent.url && ( + + {/* Filters */} +
- URL: - - {selectedAgent.url} - +
+

Search Models:

+ + } /> + + Smart search with relevance ranking - finds models containing your search terms, ranked by + relevance. Try searching 'xai grok-4', 'claude-4', 'gpt-4', or + 'sonnet' + + +
+
+ + setSearchTerm(e.target.value)} + className="border border-gray-300 rounded-lg pl-10 pr-4 py-2 w-full text-sm focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white" + /> +
+
+
+

Provider:

+ setSelectedProviders(values)} + > + + + {(values: string[]) => + values.map((provider) => ( + + {provider} + + )) + } + + + + + No providers found + + {(provider: string) => { + const { logo } = getProviderLogoAndName(provider); + return ( + + + {logo && ( + {provider} { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> + )} + {provider} + + + ); + }} + + + +
+
+

Mode:

+ +
+
+

Features:

+
- )} -
-
- - {/* Capabilities */} - {selectedAgent.capabilities && ( -
- Capabilities -
- {Object.entries(selectedAgent.capabilities) - .filter(([_, value]) => value === true) - .map(([key]) => ( - - {key} - - ))}
-
- )} - {/* Skills */} - {selectedAgent.skills && selectedAgent.skills.length > 0 && ( -
- Skills -
- {selectedAgent.skills.map((skill, index) => ( -
-
+ model.model_group || String(index)} + sortingMode="client" + sorting={modelSorting} + onSortingChange={setModelSorting} + isLoading={loading} + loadingMessage="Loading models…" + noDataMessage={ + + } + size="compact" + /> + +
+

+ Showing {filteredData.length} of {modelHubData?.length || 0} models +

+
+ + + {/* Agents Tab */} + {hasAgents && ( + +
+

Available Agents

+
+ + {/* Filters */} +
+
+
+

Search Agents:

+ + } /> + Search agents by name or description + +
+
+ + setAgentSearchTerm(e.target.value)} + className="border border-gray-300 rounded-lg pl-10 pr-4 py-2 w-full text-sm focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white" + /> +
+
+
+

Skills:

+ +
+
+ + agent.name || String(index)} + sortingMode="client" + sorting={agentSorting} + onSortingChange={setAgentSorting} + isLoading={agentLoading} + loadingMessage="Loading agents…" + noDataMessage={ + + } + size="compact" + /> + +
+

+ Showing {filteredAgentData.length} of {agentHubData?.length || 0} agents +

+
+
+ )} + + {/* MCP Servers Tab */} + {hasMcpServers && ( + +
+

Available MCP Servers

+
+ + {/* Filters */} +
+
+
+

Search MCP Servers:

+ + } /> + Search MCP servers by name or description + +
+
+ + setMcpSearchTerm(e.target.value)} + className="border border-gray-300 rounded-lg pl-10 pr-4 py-2 w-full text-sm focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white" + /> +
+
+
+

Transport:

+ +
+
+ + server.server_id || String(index)} + sortingMode="client" + sorting={mcpSorting} + onSortingChange={setMcpSorting} + isLoading={mcpLoading} + loadingMessage="Loading MCP servers…" + noDataMessage={ + + } + size="compact" + /> + +
+

+ Showing {filteredMcpData.length} of {mcpHubData?.length || 0} MCP servers +

+
+
+ )} + + {/* Skill Hub Tab */} + + + + + +
+ + {/* Model Details Modal */} + !open && handleModalCancel()}> + + + + {selectedModel?.model_group || "Model Details"} + {selectedModel && ( + + copyToClipboard(selectedModel.model_group)} + className="cursor-pointer text-gray-500 hover:text-blue-500 w-4 h-4 shrink-0" + /> + } + /> + Copy model name + + )} + + + {selectedModel && ( +
+ {/* Model Overview */} +
+

Model Overview

+
+
+

Model Name:

+

{selectedModel.model_group}

+
+
+

Mode:

+

{selectedModel.mode || "Not specified"}

+
+
+

Providers:

+
+ {(selectedModel.providers ?? []).map((provider) => { + const { logo } = getProviderLogoAndName(provider); + return ( + +
+ {logo && ( + {provider} { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> + )} + {provider} +
+
+ ); + })} +
+
+
+ + {/* Wildcard Routing Note */} + {selectedModel.model_group.includes("*") && ( +
+
+
- {skill.name} - {skill.description} +

Wildcard Routing

+

+ This model uses wildcard routing. You can pass any value where you see the{" "} + * symbol. +

+

+ For example, with{" "} + + {selectedModel.model_group} + + , you can use any string ( + + {selectedModel.model_group.replaceAll("*", "my-custom-value")} + + ) that matches this pattern. +

- {skill.tags && skill.tags.length > 0 && ( -
- {skill.tags.map((tag) => ( - - {tag} - - ))} +
+ )} +
+ + {/* Token and Cost Information */} +
+

Token & Cost Information

+
+
+

Max Input Tokens:

+

{selectedModel.max_input_tokens?.toLocaleString() || "Not specified"}

+
+
+

Max Output Tokens:

+

{selectedModel.max_output_tokens?.toLocaleString() || "Not specified"}

+
+
+

Input Cost per 1M Tokens:

+

+ {selectedModel.input_cost_per_token + ? formatCost(selectedModel.input_cost_per_token) + : "Not specified"} +

+
+
+

Output Cost per 1M Tokens:

+

+ {selectedModel.output_cost_per_token + ? formatCost(selectedModel.output_cost_per_token) + : "Not specified"} +

+
+
+
+ + {/* Capabilities */} +
+

Capabilities

+
+ {(() => { + const capabilities = getModelCapabilities(selectedModel); + + if (capabilities.length === 0) { + return

No special capabilities listed

; + } + + return capabilities.map((capability) => ( + + {formatCapabilityName(capability)} + + )); + })()} +
+
+ + {/* Rate Limits */} + {(selectedModel.tpm || selectedModel.rpm) && ( +
+

Rate Limits

+
+ {selectedModel.tpm && ( +
+

Tokens per Minute:

+

{selectedModel.tpm.toLocaleString()}

+
+ )} + {selectedModel.rpm && ( +
+

Requests per Minute:

+

{selectedModel.rpm.toLocaleString()}

)}
- ))} -
-
- )} - - {/* Input/Output Modes */} -
- Input/Output Modes -
-
- Input Modes: -
- {(selectedAgent.defaultInputModes ?? []).map((mode) => ( - - {mode} - - ))}
-
+ )} + + {/* Supported OpenAI Parameters */} + {selectedModel.supported_openai_params && selectedModel.supported_openai_params.length > 0 && ( +
+

Supported OpenAI Parameters

+
+ {selectedModel.supported_openai_params.map((param) => ( + + {param} + + ))} +
+
+ )} + + {/* Usage Example */}
- Output Modes: -
- {(selectedAgent.defaultOutputModes ?? []).map((mode) => ( - - {mode} - - ))} +

Usage Example

+
+
+                        {(() => {
+                          const codeSnippet = generateCodeSnippet({
+                            apiKeySource: "custom",
+                            accessToken: null,
+                            apiKey: "your_api_key",
+                            inputMessage: "Hello, how are you?",
+                            chatHistory: [
+                              { role: "user", content: "Hello, how are you?", isImage: false } as MessageType,
+                            ],
+                            selectedTags: [],
+                            selectedVectorStores: [],
+                            selectedGuardrails: [],
+                            selectedPolicies: [],
+                            selectedMCPServers: [],
+                            endpointType: getEndpointType(selectedModel.mode || "chat"),
+                            selectedModel: selectedModel.model_group,
+                            selectedSdk: "openai",
+                          });
+                          return codeSnippet;
+                        })()}
+                      
+
+
+
-
- - {/* Documentation */} - {selectedAgent.documentationUrl && ( -
- Documentation - - - View Documentation - -
)} + +
- {/* A2A Usage Example */} -
- Usage Example (A2A Protocol) + {/* Agent Details Modal */} + !open && handleAgentModalCancel()}> + + + + {selectedAgent?.name || "Agent Details"} + {selectedAgent && ( + + copyToClipboard(selectedAgent.name)} + className="cursor-pointer text-gray-500 hover:text-blue-500 w-4 h-4 shrink-0" + /> + } + /> + Copy agent name + + )} + + + {selectedAgent && ( +
+ {/* Agent Overview */} +
+

Agent Overview

+
+
+

Name:

+

{selectedAgent.name}

+
+
+

Version:

+

{selectedAgent.version}

+
+
+

Description:

+

{selectedAgent.description}

+
+ {selectedAgent.url && ( + + )} +
+
- {/* Step 1: Retrieve Agent Card */} -
- Step 1: Retrieve Agent Card -
-
-                      {`base_url = '${selectedAgent.url}'
+                  {/* Capabilities */}
+                  {selectedAgent.capabilities && (
+                    
+

Capabilities

+
+ {Object.entries(selectedAgent.capabilities) + .filter(([_, value]) => value === true) + .map(([key]) => ( + + {key} + + ))} +
+
+ )} + + {/* Skills */} + {selectedAgent.skills && selectedAgent.skills.length > 0 && ( +
+

Skills

+
+ {selectedAgent.skills.map((skill, index) => ( +
+
+
+

{skill.name}

+

{skill.description}

+
+
+ {skill.tags && skill.tags.length > 0 && ( +
+ {skill.tags.map((tag) => ( + + {tag} + + ))} +
+ )} +
+ ))} +
+
+ )} + + {/* Input/Output Modes */} +
+

Input/Output Modes

+
+
+

Input Modes:

+
+ {(selectedAgent.defaultInputModes ?? []).map((mode) => ( + + {mode} + + ))} +
+
+
+

Output Modes:

+
+ {(selectedAgent.defaultOutputModes ?? []).map((mode) => ( + + {mode} + + ))} +
+
+
+
+ + {/* Documentation */} + {selectedAgent.documentationUrl && ( +
+

Documentation

+ + + View Documentation + +
+ )} + + {/* A2A Usage Example */} +
+

Usage Example (A2A Protocol)

+ + {/* Step 1: Retrieve Agent Card */} +
+

Step 1: Retrieve Agent Card

+
+
+                          {`base_url = '${selectedAgent.url}'
 
 resolver = A2ACardResolver(
     httpx_client=httpx_client,
@@ -1251,12 +1275,12 @@ if _public_card.supports_authenticated_extended_card:
             f'Failed to fetch extended agent card: {e_extended}. Will proceed with public card.',
             exc_info=True,
         )`}
-                    
-
-
-
+
+
+ -
-
+ copyToClipboard(codeSnippet); + }} + className="text-sm text-blue-600 hover:text-blue-800 cursor-pointer" + > + Copy to clipboard + +
+
- {/* Step 2: Call the Agent */} -
- Step 2: Call the Agent -
-
-                      {`client = A2AClient(
+                    {/* Step 2: Call the Agent */}
+                    
+

Step 2: Call the Agent

+
+
+                          {`client = A2AClient(
     httpx_client=httpx_client, agent_card=final_agent_card_to_use
 )
 
@@ -1333,12 +1357,12 @@ request = SendMessageRequest(
 
 response = await client.send_message(request)
 print(response.model_dump(mode='json', exclude_none=True))`}
-                    
-
-
-
+
+
+ + copyToClipboard(codeSnippet); + }} + className="text-sm text-blue-600 hover:text-blue-800 cursor-pointer" + > + Copy to clipboard + +
+
-
-
- )} - - - {/* MCP Server Details Modal */} - - {selectedMcpServer?.server_name || "MCP Server Details"} - {selectedMcpServer && ( - - copyToClipboard(selectedMcpServer.server_name)} - className="cursor-pointer text-gray-500 hover:text-blue-500 w-4 h-4" - /> - )} -
- } - width={1000} - open={isMcpModalVisible} - footer={null} - onOk={handleMcpModalOk} - onCancel={handleMcpModalCancel} - > - {selectedMcpServer && ( -
- {/* Server Overview */} -
- Server Overview -
+ + + + {/* MCP Server Details Modal */} + !open && handleMcpModalCancel()}> + + + + {selectedMcpServer?.server_name || "MCP Server Details"} + {selectedMcpServer && ( + + copyToClipboard(selectedMcpServer.server_name)} + className="cursor-pointer text-gray-500 hover:text-blue-500 w-4 h-4 shrink-0" + /> + } + /> + Copy server name + + )} + + + {selectedMcpServer && ( +
+ {/* Server Overview */}
- Server Name: - {selectedMcpServer.server_name} +

Server Overview

+
+
+

Server Name:

+

{selectedMcpServer.server_name}

+
+
+

Transport:

+ {selectedMcpServer.transport} +
+ {selectedMcpServer.alias && ( +
+

Alias:

+

{selectedMcpServer.alias}

+
+ )} +
+

Auth Type:

+ + {selectedMcpServer.auth_type} + +
+
+

Description:

+

{selectedMcpServer.mcp_info?.description || "-"}

+
+
-
- Transport: - {selectedMcpServer.transport} -
- {selectedMcpServer.alias && ( + + {/* Additional Info */} + {selectedMcpServer.mcp_info && Object.keys(selectedMcpServer.mcp_info).length > 0 && (
- Alias: - {selectedMcpServer.alias} +

Additional Information

+
+
+                          {JSON.stringify(selectedMcpServer.mcp_info, null, 2)}
+                        
+
)} + + {/* Usage Example */}
- Auth Type: - - {selectedMcpServer.auth_type} - -
-
- Description: - {selectedMcpServer.mcp_info?.description || "-"} -
-
-
- - {/* Additional Info */} - {selectedMcpServer.mcp_info && Object.keys(selectedMcpServer.mcp_info).length > 0 && ( -
- Additional Information -
-
{JSON.stringify(selectedMcpServer.mcp_info, null, 2)}
-
-
- )} - - {/* Usage Example */} -
- Usage Example -
-
-                    {`# Using MCP Server with Python FastMCP
+                    

Usage Example

+
+
+                        {`# Using MCP Server with Python FastMCP
 
 from fastmcp import Client
 import asyncio
@@ -1474,12 +1501,12 @@ async def main():
 
 if __name__ == "__main__":
     asyncio.run(main())`}
-                  
-
-
-
+
+
+ + copyToClipboard(codeSnippet); + }} + className="text-sm text-blue-600 hover:text-blue-800 cursor-pointer" + > + Copy to clipboard + +
+
-
-
- )} -
- + )} + + + + ); };