Merge branch 'main' into fix-spend-logs

This commit is contained in:
OrionCodeDev 2026-02-09 22:17:00 +01:00 • committed by GitHub
commit 2accfda421
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 798 additions and 2651 deletions

View file

@ -0,0 +1,68 @@
# MAX_CALLBACKS Limit
## Error Message
```
Cannot add callback - would exceed MAX_CALLBACKS limit of 30. Current callbacks: 30
```
## What This Means
LiteLLM limits the number of callbacks that can be registered to prevent performance degradation. Each callback runs on every LLM request, so having too many callbacks can cause exponential CPU usage and slow down your proxy.
The default limit is **30 callbacks**.
## When You Might Hit This Limit
- **Large enterprise deployments** with many teams, each having their own guardrails
- **Multiple logging integrations** combined with custom callbacks
- **Per-team callback configurations** that add up across your organization
## How to Override
Set the `LITELLM_MAX_CALLBACKS` environment variable to increase the limit:
```bash
# Docker
docker run -e LITELLM_MAX_CALLBACKS=100 ...
# Docker Compose
environment:
- LITELLM_MAX_CALLBACKS=100
# Kubernetes
env:
- name: LITELLM_MAX_CALLBACKS
value: "100"
# Direct
export LITELLM_MAX_CALLBACKS=100
litellm --config config.yaml
```
## Recommendations
1. **Start conservative** - Only increase as much as you need. If you have 60 teams with guardrails, try `LITELLM_MAX_CALLBACKS=75` to leave headroom.
2. **Monitor performance** - More callbacks means more processing per request. Watch your CPU usage and response latency after increasing the limit.
3. **Consolidate where possible** - If multiple teams use identical guardrails, consider using shared callback configurations rather than per-team duplicates.
## Example: Large Enterprise Setup
For an organization with 60+ teams, each with a guardrail callback:
```yaml
# config.yaml
litellm_settings:
callbacks: ["prometheus", "langfuse"] # 2 global callbacks
# Each team adds 1 guardrail callback = 60+ callbacks
# Total: 62+ callbacks needed
```
Set the environment variable:
```bash
export LITELLM_MAX_CALLBACKS=100
```

View file

@ -27,7 +27,7 @@ import TabItem from '@theme/TabItem';
docker run \
-e STORE_MODEL_IN_DB=True \
-p 4000:4000 \
docker.litellm.ai/berriai/litellm:v1.81.3.rc.2
docker.litellm.ai/berriai/litellm:v1.81.3-stable
```
</TabItem>

View file

@ -1081,6 +1081,7 @@ const sidebars = {
"troubleshoot/cpu_issues",
"troubleshoot/memory_issues",
"troubleshoot/spend_queue_warnings",
"troubleshoot/max_callbacks",
],
},
],

View file

@ -2,6 +2,8 @@ import os
import sys
from typing import List, Literal
from litellm.litellm_core_utils.env_utils import get_env_int
DEFAULT_HEALTH_CHECK_PROMPT = str(
os.getenv("DEFAULT_HEALTH_CHECK_PROMPT", "test from litellm")
)
@ -99,6 +101,11 @@ DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET_GEMINI_2_5_FLASH_LITE = int(
)
)
# Maximum number of callbacks that can be registered
# This prevents callbacks from exponentially growing and consuming CPU resources
# Override with LITELLM_MAX_CALLBACKS env var for large deployments (e.g., many teams with guardrails)
MAX_CALLBACKS = get_env_int("LITELLM_MAX_CALLBACKS", 30)
# Generic fallback for unknown models
DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET = int(
os.getenv("DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET", 128)

View file

@ -0,0 +1,21 @@
"""
Utility helpers for reading and parsing environment variables.
"""
import os
def get_env_int(env_var: str, default: int) -> int:
"""Parse an environment variable as an integer, falling back to default on invalid values.
Handles empty strings, whitespace, and non-numeric values gracefully
so that misconfiguration doesn't crash the process at import time.
"""
raw = os.getenv(env_var)
if raw is None:
return default
raw = raw.strip()
try:
return int(raw)
except (ValueError, TypeError):
return default

View file

@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Set, Type, Uni
import litellm
from litellm._logging import verbose_logger
from litellm.constants import MAX_CALLBACKS
from litellm.integrations.additional_logging_utils import AdditionalLoggingUtils
from litellm.integrations.custom_logger import CustomLogger
from litellm.integrations.generic_api.generic_api_callback import GenericAPILogger
@ -24,9 +25,6 @@ class LoggingCallbackManager:
- Keep a reasonable MAX_CALLBACKS limit (this ensures callbacks don't exponentially grow and consume CPU Resources)
"""
# healthy maximum number of callbacks - unlikely someone needs more than 20
MAX_CALLBACKS = 30
def add_litellm_input_callback(self, callback: Union[CustomLogger, str]):
"""
Add a input callback to litellm.input_callback
@ -155,9 +153,9 @@ class LoggingCallbackManager:
Check if adding another callback would exceed MAX_CALLBACKS
Returns True if safe to add, False if would exceed limit
"""
if len(parent_list) >= self.MAX_CALLBACKS:
if len(parent_list) >= MAX_CALLBACKS:
verbose_logger.warning(
f"Cannot add callback - would exceed MAX_CALLBACKS limit of {self.MAX_CALLBACKS}. Current callbacks: {len(parent_list)}"
f"Cannot add callback - would exceed MAX_CALLBACKS limit of {MAX_CALLBACKS}. Current callbacks: {len(parent_list)}"
)
return False
return True

View file

@ -0,0 +1,124 @@
/* @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 mockAccessToken = "test-token-456";
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: mockAccessToken,
} as any);
});
it("should return hook result without errors", () => {
vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]);
const { result } = renderHook(() => useMCPAccessGroups(), { wrapper });
expect(result.current).toBeDefined();
expect(result.current).toHaveProperty("data");
expect(result.current).toHaveProperty("isSuccess");
expect(result.current).toHaveProperty("isError");
expect(result.current).toHaveProperty("status");
});
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(mockAccessToken);
expect(result.current.data).toEqual(mockAccessGroups);
});
it("should not fetch when access token is null", 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.isFetching).toBe(false);
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(networking.fetchMCPAccessGroups).not.toHaveBeenCalled();
});
it("should not fetch when access token is empty string", async () => {
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
vi.mocked(useAuthorizedModule.default).mockReturnValue({
accessToken: "",
} as any);
const { result } = renderHook(() => useMCPAccessGroups(), { wrapper });
expect(result.current.isFetching).toBe(false);
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
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);
expect(result.current.data).toBeUndefined();
});
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([]);
});
});

View file

@ -0,0 +1,134 @@
/* @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("@/app/(dashboard)/hooks/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 mockAccessToken = "test-token-123";
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("@/app/(dashboard)/hooks/useAuthorized");
vi.mocked(useAuthorizedModule.default).mockReturnValue({
accessToken: mockAccessToken,
} as any);
});
it("should return hook result without errors", () => {
vi.mocked(networking.fetchMCPServers).mockResolvedValue([]);
const { result } = renderHook(() => useMCPServers(), { wrapper });
expect(result.current).toBeDefined();
expect(result.current).toHaveProperty("data");
expect(result.current).toHaveProperty("isSuccess");
expect(result.current).toHaveProperty("isError");
expect(result.current).toHaveProperty("status");
});
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(mockAccessToken);
expect(result.current.data).toEqual(mockServers);
});
it("should not fetch when access token is null", async () => {
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
vi.mocked(useAuthorizedModule.default).mockReturnValue({
accessToken: null,
} as any);
const { result } = renderHook(() => useMCPServers(), { wrapper });
expect(result.current.isFetching).toBe(false);
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(networking.fetchMCPServers).not.toHaveBeenCalled();
});
it("should not fetch when access token is empty string", async () => {
const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized");
vi.mocked(useAuthorizedModule.default).mockReturnValue({
accessToken: "",
} as any);
const { result } = renderHook(() => useMCPServers(), { wrapper });
expect(result.current.isFetching).toBe(false);
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
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);
expect(result.current.data).toBeUndefined();
});
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([]);
});
});

View file

@ -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;

View file

@ -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;

View file

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

View file

@ -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;

View file

@ -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">&apos;{credentialName}&apos;</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;

View file

@ -1,4 +1,4 @@
import { screen, waitFor } from "@testing-library/react";
import { screen, waitFor, within } 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";
@ -58,8 +58,34 @@ describe("FilterComponent", () => {
expect(screen.getByRole("button", { name: "Custom Filters" })).toBeInTheDocument();
});
it("should toggle filters visibility when filter button is clicked", async () => {
const user = userEvent.setup({ delay: null });
renderWithProviders(
<FilterComponent
options={defaultOptions}
onApplyFilters={mockOnApplyFilters}
onResetFilters={mockOnResetFilters}
/>,
);
const filterButton = screen.getByRole("button", { name: "Filters" });
expect(screen.queryByPlaceholderText("Enter User ID...")).not.toBeInTheDocument();
await user.click(filterButton);
await waitFor(() => {
expect(screen.getByPlaceholderText("Enter User ID...")).toBeInTheDocument();
});
await user.click(filterButton);
await waitFor(() => {
expect(screen.queryByPlaceholderText("Enter User ID...")).not.toBeInTheDocument();
});
});
it("should call onResetFilters when reset button is clicked", async () => {
const user = userEvent.setup();
const user = userEvent.setup({ delay: null });
renderWithProviders(
<FilterComponent
options={defaultOptions}
@ -78,7 +104,7 @@ describe("FilterComponent", () => {
});
it("should render filters in correct order", async () => {
const user = userEvent.setup();
const user = userEvent.setup({ delay: null });
const options: FilterOption[] = [
{ name: "model", label: "Model" },
{ name: "teamId", label: "Team ID" },
@ -105,7 +131,7 @@ describe("FilterComponent", () => {
});
it("should handle input filter changes", async () => {
const user = userEvent.setup();
const user = userEvent.setup({ delay: null });
renderWithProviders(
<FilterComponent
options={defaultOptions}
@ -124,4 +150,378 @@ describe("FilterComponent", () => {
expect(mockOnApplyFilters).toHaveBeenCalledWith({ userId: "user123" });
});
});
it("should display initial values in filters", async () => {
const user = userEvent.setup({ delay: null });
renderWithProviders(
<FilterComponent
options={defaultOptions}
onApplyFilters={mockOnApplyFilters}
onResetFilters={mockOnResetFilters}
initialValues={{ teamId: "team1", userId: "user123" }}
/>,
);
const filterButton = screen.getByRole("button", { name: "Filters" });
await user.click(filterButton);
await waitFor(() => {
const userIdInput = screen.getByPlaceholderText("Enter User ID...") as HTMLInputElement;
expect(userIdInput.value).toBe("user123");
});
});
it("should handle select dropdown filter changes", async () => {
const user = userEvent.setup({ delay: null });
renderWithProviders(
<FilterComponent
options={defaultOptions}
onApplyFilters={mockOnApplyFilters}
onResetFilters={mockOnResetFilters}
/>,
);
const filterButton = screen.getByRole("button", { name: "Filters" });
await user.click(filterButton);
const teamIdLabel = screen.getByText("Team ID");
const teamIdSection = teamIdLabel.closest("div");
const teamIdSelect = within(teamIdSection!).getByRole("combobox");
await user.click(teamIdSelect);
await waitFor(() => {
expect(screen.getByText("Team 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Team 1"));
await waitFor(() => {
expect(mockOnApplyFilters).toHaveBeenCalledWith({ teamId: "team1" });
});
});
it("should handle searchable filter with search function", async () => {
const user = userEvent.setup({ delay: null });
const mockSearchFn = vi.fn().mockResolvedValue([
{ label: "Result 1", value: "result1" },
{ label: "Result 2", value: "result2" },
]);
const options: FilterOption[] = [
{
name: "model",
label: "Model",
isSearchable: true,
searchFn: mockSearchFn,
},
];
renderWithProviders(
<FilterComponent
options={options}
onApplyFilters={mockOnApplyFilters}
onResetFilters={mockOnResetFilters}
/>,
);
const filterButton = screen.getByRole("button", { name: "Filters" });
await user.click(filterButton);
await waitFor(() => {
expect(mockSearchFn).toHaveBeenCalledWith("");
});
const modelLabel = screen.getByText("Model");
const modelSection = modelLabel.closest("div");
const modelSelect = within(modelSection!).getByRole("combobox");
await user.click(modelSelect);
await waitFor(() => {
expect(screen.getByText("Result 1")).toBeInTheDocument();
expect(screen.getByText("Result 2")).toBeInTheDocument();
});
});
it("should debounce search input for searchable filters", async () => {
const user = userEvent.setup({ delay: null });
const mockSearchFn = vi.fn().mockResolvedValue([
{ label: "Result", value: "result" },
]);
const options: FilterOption[] = [
{
name: "model",
label: "Model",
isSearchable: true,
searchFn: mockSearchFn,
},
];
renderWithProviders(
<FilterComponent
options={options}
onApplyFilters={mockOnApplyFilters}
onResetFilters={mockOnResetFilters}
/>,
);
const filterButton = screen.getByRole("button", { name: "Filters" });
await user.click(filterButton);
await waitFor(() => {
expect(mockSearchFn).toHaveBeenCalledWith("");
});
vi.clearAllMocks();
const modelLabel = screen.getByText("Model");
const modelSection = modelLabel.closest("div");
const modelSelect = within(modelSection!).getByRole("combobox");
await user.click(modelSelect);
await user.type(modelSelect, "test");
expect(mockSearchFn).not.toHaveBeenCalled();
await waitFor(
() => {
expect(mockSearchFn).toHaveBeenCalledWith("test");
},
{ timeout: 500 },
);
});
it("should show loading state when searching", async () => {
const user = userEvent.setup({ delay: null });
let resolveSearch: (value: Array<{ label: string; value: string }>) => void;
const mockSearchFn = vi.fn().mockImplementation(
() =>
new Promise<Array<{ label: string; value: string }>>((resolve) => {
resolveSearch = resolve;
}),
);
const options: FilterOption[] = [
{
name: "model",
label: "Model",
isSearchable: true,
searchFn: mockSearchFn,
},
];
renderWithProviders(
<FilterComponent
options={options}
onApplyFilters={mockOnApplyFilters}
onResetFilters={mockOnResetFilters}
/>,
);
const filterButton = screen.getByRole("button", { name: "Filters" });
await user.click(filterButton);
await waitFor(() => {
expect(mockSearchFn).toHaveBeenCalledWith("");
});
const modelLabel = screen.getByText("Model");
const modelSection = modelLabel.closest("div");
const modelSelect = within(modelSection!).getByRole("combobox");
await user.click(modelSelect);
await user.type(modelSelect, "test");
await waitFor(
() => {
expect(screen.getByText("Loading...")).toBeInTheDocument();
},
{ timeout: 500 },
);
resolveSearch!([{ label: "Result", value: "result" }]);
await waitFor(() => {
expect(screen.queryByText("Loading...")).not.toBeInTheDocument();
});
});
it("should handle search errors gracefully", async () => {
const user = userEvent.setup({ delay: null });
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const mockSearchFn = vi.fn().mockRejectedValue(new Error("Search failed"));
const options: FilterOption[] = [
{
name: "model",
label: "Model",
isSearchable: true,
searchFn: mockSearchFn,
},
];
renderWithProviders(
<FilterComponent
options={options}
onApplyFilters={mockOnApplyFilters}
onResetFilters={mockOnResetFilters}
/>,
);
const filterButton = screen.getByRole("button", { name: "Filters" });
await user.click(filterButton);
await waitFor(() => {
expect(mockSearchFn).toHaveBeenCalledWith("");
});
const modelLabel = screen.getByText("Model");
const modelSection = modelLabel.closest("div");
const modelSelect = within(modelSection!).getByRole("combobox");
await user.click(modelSelect);
await user.type(modelSelect, "test");
await waitFor(
() => {
expect(consoleErrorSpy).toHaveBeenCalledWith("Error searching:", expect.any(Error));
expect(screen.getByText("No results found")).toBeInTheDocument();
},
{ timeout: 500 },
);
consoleErrorSpy.mockRestore();
});
it("should load initial options when dropdown opens for searchable filter", async () => {
const user = userEvent.setup({ delay: null });
const mockSearchFn = vi.fn().mockResolvedValue([
{ label: "Initial Result", value: "initial" },
]);
const options: FilterOption[] = [
{
name: "model",
label: "Model",
isSearchable: true,
searchFn: mockSearchFn,
},
];
renderWithProviders(
<FilterComponent
options={options}
onApplyFilters={mockOnApplyFilters}
onResetFilters={mockOnResetFilters}
/>,
);
const filterButton = screen.getByRole("button", { name: "Filters" });
await user.click(filterButton);
await waitFor(() => {
expect(mockSearchFn).toHaveBeenCalledWith("");
});
vi.clearAllMocks();
const modelLabel = screen.getByText("Model");
const modelSection = modelLabel.closest("div");
const modelSelect = within(modelSection!).getByRole("combobox");
await user.click(modelSelect);
await waitFor(() => {
expect(screen.getByText("Initial Result")).toBeInTheDocument();
});
});
it("should not render filters that are not in orderedFilters list", async () => {
const user = userEvent.setup({ delay: null });
const options: FilterOption[] = [
{
name: "unknownFilter",
label: "Unknown Filter",
},
];
renderWithProviders(
<FilterComponent
options={options}
onApplyFilters={mockOnApplyFilters}
onResetFilters={mockOnResetFilters}
/>,
);
const filterButton = screen.getByRole("button", { name: "Filters" });
await user.click(filterButton);
await waitFor(() => {
expect(screen.queryByText("Unknown Filter")).not.toBeInTheDocument();
});
});
it("should call onApplyFilters with updated values when multiple filters change", async () => {
const user = userEvent.setup({ delay: null });
renderWithProviders(
<FilterComponent
options={defaultOptions}
onApplyFilters={mockOnApplyFilters}
onResetFilters={mockOnResetFilters}
/>,
);
const filterButton = screen.getByRole("button", { name: "Filters" });
await user.click(filterButton);
const userIdInput = screen.getByPlaceholderText("Enter User ID...");
await user.type(userIdInput, "user123");
await waitFor(() => {
expect(mockOnApplyFilters).toHaveBeenCalledWith({ userId: "user123" });
});
const teamIdLabel = screen.getByText("Team ID");
const teamIdSection = teamIdLabel.closest("div");
const teamIdSelect = within(teamIdSection!).getByRole("combobox");
await user.click(teamIdSelect);
await waitFor(() => {
expect(screen.getByText("Team 1")).toBeInTheDocument();
});
await user.click(screen.getByText("Team 1"));
await waitFor(() => {
expect(mockOnApplyFilters).toHaveBeenCalledWith({
userId: "user123",
teamId: "team1",
});
});
});
it("should reset all filter values when reset button is clicked", async () => {
const user = userEvent.setup({ delay: null });
renderWithProviders(
<FilterComponent
options={defaultOptions}
onApplyFilters={mockOnApplyFilters}
onResetFilters={mockOnResetFilters}
initialValues={{ teamId: "team1", userId: "user123" }}
/>,
);
const filterButton = screen.getByRole("button", { name: "Filters" });
await user.click(filterButton);
await waitFor(() => {
const userIdInput = screen.getByPlaceholderText("Enter User ID...") as HTMLInputElement;
expect(userIdInput.value).toBe("user123");
});
const resetButton = screen.getByRole("button", { name: "Reset Filters" });
await user.click(resetButton);
await waitFor(() => {
const userIdInput = screen.getByPlaceholderText("Enter User ID...") as HTMLInputElement;
expect(userIdInput.value).toBe("");
});
});
});

View file

@ -164,7 +164,7 @@ const FilterComponent: React.FC<FilterComponentProps> = ({
placeholder={`Search ${option.label || option.name}...`}
value={tempValues[option.name] || undefined}
onChange={(value) => handleFilterChange(option.name, value)}
onDropdownVisibleChange={(open) => handleDropdownVisibleChange(open, option)}
onOpenChange={(open) => handleDropdownVisibleChange(open, option)}
onSearch={(value) => {
setSearchInputValueMap((prev) => ({
...prev,

View file

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

View file

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

View file

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

View file

@ -0,0 +1,21 @@
export const ERROR_CODE_OPTIONS: { label: string; value: string }[] = [
{ label: "400 - Bad Request", value: "400" },
{ label: "401 - Invalid Authentication", value: "401" },
{ label: "403 - Permission Denied", value: "403" },
{ label: "404 - Not Found", value: "404" },
{ label: "408 - Request Timeout", value: "408" },
{ label: "422 - Unprocessable Entity", value: "422" },
{ label: "429 - Rate Limited", value: "429" },
{ label: "500 - Internal Server Error", value: "500" },
{ label: "502 - Bad Gateway", value: "502" },
{ label: "503 - Service Unavailable", value: "503" },
{ label: "529 - Overloaded", value: "529" },
];
export const QUICK_SELECT_OPTIONS: { label: string; value: number; unit: string }[] = [
{ label: "Last 15 Minutes", value: 15, unit: "minutes" },
{ label: "Last Hour", value: 1, unit: "hours" },
{ label: "Last 4 Hours", value: 4, unit: "hours" },
{ label: "Last 24 Hours", value: 24, unit: "hours" },
{ label: "Last 7 Days", value: 7, unit: "days" },
];

View file

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

View file

@ -24,6 +24,7 @@ import { CostBreakdownViewer } from "./CostBreakdownViewer";
import { ErrorViewer } from "./ErrorViewer";
import { useLogFilterLogic } from "./log_filter_logic";
import { getTimeRangeDisplay } from "./logs_utils";
import { ERROR_CODE_OPTIONS, QUICK_SELECT_OPTIONS } from "./constants";
import { RequestResponsePanel } from "./RequestResponsePanel";
import { SessionView } from "./SessionView";
import SpendLogsSettingsModal from "./SpendLogsSettingsModal/SpendLogsSettingsModal";
@ -354,24 +355,6 @@ export default function SpendLogsTable({
setSelectedLog(log);
};
// Function to extract unique error codes from logs
const extractErrorCodes = (logs: LogEntry[], searchText: string = "") => {
const errorCodes = new Set<string>();
logs.forEach((log) => {
const metadata = log.metadata || {};
if (metadata.status === "failure" && metadata.error_information) {
const errorCode = metadata.error_information.error_code;
if (errorCode && (!searchText || errorCode.toLowerCase().includes(searchText.toLowerCase()))) {
errorCodes.add(errorCode);
}
}
});
return Array.from(errorCodes).map((code) => ({
label: code,
value: code,
}));
};
const logFilterOptions: FilterOption[] = [
{
name: "Team ID",
@ -437,7 +420,14 @@ export default function SpendLogsTable({
label: "Error Code",
isSearchable: true,
searchFn: async (searchText: string) => {
return extractErrorCodes(logsData.data, searchText);
if (!searchText) return ERROR_CODE_OPTIONS;
const lower = searchText.toLowerCase();
const filtered = ERROR_CODE_OPTIONS.filter((opt) => opt.label.toLowerCase().includes(lower));
const isExactValue = ERROR_CODE_OPTIONS.some((opt) => opt.value === searchText.trim());
if (!isExactValue && searchText.trim()) {
filtered.push({ label: `Use custom code: ${searchText.trim()}`, value: searchText.trim() });
}
return filtered;
},
},
{
@ -474,15 +464,7 @@ export default function SpendLogsTable({
return unit;
};
const quickSelectOptions = [
{ label: "Last 15 Minutes", value: 15, unit: "minutes" },
{ label: "Last Hour", value: 1, unit: "hours" },
{ label: "Last 4 Hours", value: 4, unit: "hours" },
{ label: "Last 24 Hours", value: 24, unit: "hours" },
{ label: "Last 7 Days", value: 7, unit: "days" },
];
const selectedOption = quickSelectOptions.find(
const selectedOption = QUICK_SELECT_OPTIONS.find(
(option) => option.value === selectedTimeInterval.value && option.unit === selectedTimeInterval.unit,
);
@ -599,7 +581,7 @@ export default function SpendLogsTable({
{quickSelectOpen && (
<div className="absolute right-0 mt-2 w-64 bg-white rounded-lg shadow-lg border p-2 z-50">
<div className="space-y-1">
{quickSelectOptions.map((option) => (
{QUICK_SELECT_OPTIONS.map((option) => (
<button
key={option.label}
className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-50 rounded-md ${displayLabel === option.label ? "bg-blue-50 text-blue-600" : ""

View file

@ -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",