[Refactor] UI: Remove 38 unused files detected by knip

Dead code cleanup — these files had no imports from any active entry points.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yuneng-jiang
2026-02-19 22:07:41 -08:00
parent 37c98f8325
commit eeab8705a1
38 changed files with 0 additions and 4668 deletions
@@ -1,155 +0,0 @@
import {
formatInstallCommand,
getCategoryBadgeColor,
getSourceLink
} from "@/components/claude_code_plugins/helpers";
import { MarketplacePluginEntry } from "@/components/claude_code_plugins/types";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { ExternalLinkIcon } from "@heroicons/react/outline";
import { CopyOutlined } from "@ant-design/icons";
import { Badge, Button, Card, Text } from "@tremor/react";
import { Tooltip } from "antd";
import React from "react";
interface PluginCardProps {
plugin: MarketplacePluginEntry;
}
const PluginCard: React.FC<PluginCardProps> = ({ plugin }) => {
const installCommand = formatInstallCommand(plugin);
const sourceLink = getSourceLink(plugin.source);
const categoryBadgeColor = getCategoryBadgeColor(plugin.category);
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
NotificationsManager.success("Install command copied!");
};
// Limit keywords display to first 5
const displayKeywords = plugin.keywords?.slice(0, 5) || [];
const remainingKeywords = (plugin.keywords?.length || 0) - 5;
return (
<Card
className="hover:shadow-lg transition-shadow duration-200 h-full flex flex-col"
decoration="top"
decorationColor={categoryBadgeColor}
>
{/* Header */}
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="text-lg font-semibold text-gray-900">
{plugin.name}
</h3>
{plugin.version && (
<Badge color="blue" size="xs">
v{plugin.version}
</Badge>
)}
{plugin.category && (
<Badge color={categoryBadgeColor} size="xs">
{plugin.category}
</Badge>
)}
</div>
</div>
{sourceLink && (
<Tooltip title="View source repository">
<a
href={sourceLink}
target="_blank"
rel="noopener noreferrer"
className="text-gray-500 hover:text-blue-500"
onClick={(e) => e.stopPropagation()}
>
<ExternalLinkIcon className="h-5 w-5" />
</a>
</Tooltip>
)}
</div>
{/* Description */}
<div className="mb-4 flex-1">
{plugin.description ? (
<Text className="text-sm text-gray-600 line-clamp-3">
{plugin.description}
</Text>
) : (
<Text className="text-sm text-gray-400 italic">
No description available
</Text>
)}
</div>
{/* Keywords */}
{displayKeywords.length > 0 && (
<div className="flex flex-wrap gap-1 mb-4">
{displayKeywords.map((keyword, index) => (
<Badge key={index} color="gray" size="xs" className="text-xs">
{keyword}
</Badge>
))}
{remainingKeywords > 0 && (
<Badge color="gray" size="xs" className="text-xs">
+{remainingKeywords} more
</Badge>
)}
</div>
)}
{/* Author */}
{plugin.author && (
<div className="mb-4">
<Text className="text-xs text-gray-500">
By {plugin.author.name}
{plugin.author.email && ` (${plugin.author.email})`}
</Text>
</div>
)}
{/* Homepage Link */}
{plugin.homepage && (
<div className="mb-4">
<a
href={plugin.homepage}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-500 hover:text-blue-700 flex items-center gap-1"
onClick={(e) => e.stopPropagation()}
>
Visit homepage
<ExternalLinkIcon className="h-3 w-3" />
</a>
</div>
)}
{/* Install Command */}
<div className="mt-auto pt-4 border-t border-gray-100">
<div className="flex items-center justify-between gap-2">
<div className="flex-1 overflow-hidden">
<Text className="text-xs text-gray-500 mb-1">Install command</Text>
<Tooltip title={installCommand}>
<code className="block text-xs bg-gray-50 px-2 py-1 rounded font-mono text-gray-700 truncate">
{installCommand}
</code>
</Tooltip>
</div>
<Tooltip title="Copy install command">
<Button
size="xs"
variant="secondary"
icon={CopyOutlined}
onClick={(e) => {
e.stopPropagation();
copyToClipboard(installCommand);
}}
/>
</Tooltip>
</div>
</div>
</Card>
);
};
export default PluginCard;
@@ -1,203 +0,0 @@
import React from "react";
import { Text } from "@tremor/react";
import { Card, Statistic, Row, Col, Divider, Spin } from "antd";
import { DollarOutlined, LoadingOutlined } from "@ant-design/icons";
import { CostEstimateResponse } from "../types";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import ExportDropdown from "./export_dropdown";
interface CostResultsProps {
result: CostEstimateResponse | null;
loading: boolean;
}
const formatCost = (value: number | null | undefined): string => {
if (value === null || value === undefined) return "-";
if (value === 0) return "$0";
if (value < 0.0001) return `$${value.toExponential(2)}`;
if (value < 1) return `$${value.toFixed(4)}`;
return `$${formatNumberWithCommas(value, 2, true)}`;
};
const formatRequests = (value: number | null | undefined): string => {
if (value === null || value === undefined) return "-";
return formatNumberWithCommas(value, 0, true);
};
const CostResults: React.FC<CostResultsProps> = ({ result, loading }) => {
if (!result && !loading) {
return (
<div className="py-8 text-center border border-dashed border-gray-300 rounded-lg">
<Text className="text-gray-500">
Select a model to see cost estimates
</Text>
</div>
);
}
if (loading && !result) {
return (
<div className="py-8 text-center">
<Spin indicator={<LoadingOutlined spin />} />
<Text className="text-gray-500 block mt-2">Calculating costs...</Text>
</div>
);
}
if (!result) return null;
return (
<div className="space-y-4">
<Divider />
<div className="mb-4 flex items-center justify-between">
<div>
<Text className="text-lg font-semibold text-gray-900">Cost Estimate</Text>
<Text className="text-sm text-gray-500 block mt-1">
Model: {result.model} {result.provider && `(${result.provider})`}
</Text>
</div>
<div className="flex items-center gap-2">
{loading && <Spin indicator={<LoadingOutlined spin />} size="small" />}
<ExportDropdown result={result} />
</div>
</div>
<Card size="small" title="Per-Request Cost Breakdown">
<Row gutter={16}>
<Col span={6}>
<Statistic
title="Total Cost"
value={formatCost(result.cost_per_request)}
valueStyle={{ color: "#1890ff", fontSize: "18px" }}
prefix={<DollarOutlined />}
/>
</Col>
<Col span={6}>
<Statistic
title="Input Cost"
value={formatCost(result.input_cost_per_request)}
valueStyle={{ fontSize: "16px" }}
/>
</Col>
<Col span={6}>
<Statistic
title="Output Cost"
value={formatCost(result.output_cost_per_request)}
valueStyle={{ fontSize: "16px" }}
/>
</Col>
<Col span={6}>
<Statistic
title="Margin/Fee"
value={formatCost(result.margin_cost_per_request)}
valueStyle={{
fontSize: "16px",
color: result.margin_cost_per_request > 0 ? "#faad14" : undefined,
}}
/>
</Col>
</Row>
</Card>
{result.daily_cost !== null && (
<Card
size="small"
title={`Daily Costs (${formatRequests(result.num_requests_per_day)} requests/day)`}
>
<Row gutter={16}>
<Col span={6}>
<Statistic
title="Total Daily"
value={formatCost(result.daily_cost)}
valueStyle={{ color: "#52c41a", fontSize: "18px" }}
prefix={<DollarOutlined />}
/>
</Col>
<Col span={6}>
<Statistic
title="Input Cost"
value={formatCost(result.daily_input_cost)}
valueStyle={{ fontSize: "16px" }}
/>
</Col>
<Col span={6}>
<Statistic
title="Output Cost"
value={formatCost(result.daily_output_cost)}
valueStyle={{ fontSize: "16px" }}
/>
</Col>
<Col span={6}>
<Statistic
title="Margin/Fee"
value={formatCost(result.daily_margin_cost)}
valueStyle={{
fontSize: "16px",
color: (result.daily_margin_cost ?? 0) > 0 ? "#faad14" : undefined,
}}
/>
</Col>
</Row>
</Card>
)}
{result.monthly_cost !== null && (
<Card
size="small"
title={`Monthly Costs (${formatRequests(result.num_requests_per_month)} requests/month)`}
>
<Row gutter={16}>
<Col span={6}>
<Statistic
title="Total Monthly"
value={formatCost(result.monthly_cost)}
valueStyle={{ color: "#722ed1", fontSize: "18px" }}
prefix={<DollarOutlined />}
/>
</Col>
<Col span={6}>
<Statistic
title="Input Cost"
value={formatCost(result.monthly_input_cost)}
valueStyle={{ fontSize: "16px" }}
/>
</Col>
<Col span={6}>
<Statistic
title="Output Cost"
value={formatCost(result.monthly_output_cost)}
valueStyle={{ fontSize: "16px" }}
/>
</Col>
<Col span={6}>
<Statistic
title="Margin/Fee"
value={formatCost(result.monthly_margin_cost)}
valueStyle={{
fontSize: "16px",
color: (result.monthly_margin_cost ?? 0) > 0 ? "#faad14" : undefined,
}}
/>
</Col>
</Row>
</Card>
)}
{(result.input_cost_per_token || result.output_cost_per_token) && (
<div className="text-sm text-gray-500 mt-4">
<Text className="font-medium">Token Pricing: </Text>
{result.input_cost_per_token && (
<span>Input: ${formatNumberWithCommas(result.input_cost_per_token * 1_000_000, 2)}/1M tokens</span>
)}
{result.input_cost_per_token && result.output_cost_per_token && " | "}
{result.output_cost_per_token && (
<span>Output: ${formatNumberWithCommas(result.output_cost_per_token * 1_000_000, 2)}/1M tokens</span>
)}
</div>
)}
</div>
);
};
export default CostResults;
@@ -1,71 +0,0 @@
import React, { useState, useRef, useEffect } from "react";
import { Button } from "@tremor/react";
import { DownloadOutlined, FilePdfOutlined, FileExcelOutlined } from "@ant-design/icons";
import { CostEstimateResponse } from "../types";
import { exportToPDF, exportToCSV } from "./export_utils";
interface ExportDropdownProps {
result: CostEstimateResponse;
}
const ExportDropdown: React.FC<ExportDropdownProps> = ({ result }) => {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
}
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOpen]);
return (
<div className="relative inline-block" ref={menuRef}>
<Button
size="xs"
variant="secondary"
icon={DownloadOutlined}
onClick={() => setIsOpen(!isOpen)}
>
Export
</Button>
{isOpen && (
<div className="absolute right-0 mt-1 w-44 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
<button
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
onClick={() => {
exportToPDF(result);
setIsOpen(false);
}}
>
<FilePdfOutlined className="mr-3 text-red-500" />
Export as PDF
</button>
<button
className="flex items-center w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 transition-colors"
onClick={() => {
exportToCSV(result);
setIsOpen(false);
}}
>
<FileExcelOutlined className="mr-3 text-green-600" />
Export as CSV
</button>
</div>
)}
</div>
);
};
export default ExportDropdown;
@@ -1,276 +0,0 @@
import { CostEstimateResponse } from "../types";
import { formatNumberWithCommas } from "@/utils/dataUtils";
const formatCostForExport = (value: number | null | undefined): string => {
if (value === null || value === undefined) return "-";
if (value === 0) return "$0.00";
if (value < 0.01) return `$${value.toFixed(6)}`;
if (value < 1) return `$${value.toFixed(4)}`;
return `$${formatNumberWithCommas(value, 2)}`;
};
const formatRequestsForExport = (value: number | null | undefined): string => {
if (value === null || value === undefined) return "-";
return formatNumberWithCommas(value, 0);
};
export const exportToPDF = (result: CostEstimateResponse): void => {
const printWindow = window.open("", "_blank");
if (!printWindow) {
alert("Please allow popups to export PDF");
return;
}
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Cost Estimate Report - ${result.model}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 40px;
max-width: 800px;
margin: 0 auto;
color: #333;
}
h1 {
color: #1a1a1a;
border-bottom: 2px solid #1890ff;
padding-bottom: 10px;
margin-bottom: 30px;
}
h2 {
color: #444;
margin-top: 30px;
margin-bottom: 15px;
}
.meta {
background: #f5f5f5;
padding: 15px;
border-radius: 8px;
margin-bottom: 30px;
}
.meta p {
margin: 5px 0;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background: #f8f9fa;
font-weight: 600;
}
.cost-value {
font-family: monospace;
font-size: 14px;
}
.total-row {
font-weight: bold;
background: #e6f7ff;
}
.footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #ddd;
font-size: 12px;
color: #666;
}
@media print {
body { padding: 20px; }
}
</style>
</head>
<body>
<h1>🚅 LiteLLM Cost Estimate Report</h1>
<div class="meta">
<p><strong>Model:</strong> ${result.model}</p>
${result.provider ? `<p><strong>Provider:</strong> ${result.provider}</p>` : ""}
<p><strong>Input Tokens per Request:</strong> ${formatRequestsForExport(result.input_tokens)}</p>
<p><strong>Output Tokens per Request:</strong> ${formatRequestsForExport(result.output_tokens)}</p>
${result.num_requests_per_day ? `<p><strong>Requests per Day:</strong> ${formatRequestsForExport(result.num_requests_per_day)}</p>` : ""}
${result.num_requests_per_month ? `<p><strong>Requests per Month:</strong> ${formatRequestsForExport(result.num_requests_per_month)}</p>` : ""}
</div>
<h2>Per-Request Cost Breakdown</h2>
<table>
<tr>
<th>Cost Type</th>
<th>Amount</th>
</tr>
<tr>
<td>Input Cost</td>
<td class="cost-value">${formatCostForExport(result.input_cost_per_request)}</td>
</tr>
<tr>
<td>Output Cost</td>
<td class="cost-value">${formatCostForExport(result.output_cost_per_request)}</td>
</tr>
<tr>
<td>Margin/Fee</td>
<td class="cost-value">${formatCostForExport(result.margin_cost_per_request)}</td>
</tr>
<tr class="total-row">
<td>Total per Request</td>
<td class="cost-value">${formatCostForExport(result.cost_per_request)}</td>
</tr>
</table>
${result.daily_cost !== null ? `
<h2>Daily Cost Estimate (${formatRequestsForExport(result.num_requests_per_day)} requests/day)</h2>
<table>
<tr>
<th>Cost Type</th>
<th>Amount</th>
</tr>
<tr>
<td>Input Cost</td>
<td class="cost-value">${formatCostForExport(result.daily_input_cost)}</td>
</tr>
<tr>
<td>Output Cost</td>
<td class="cost-value">${formatCostForExport(result.daily_output_cost)}</td>
</tr>
<tr>
<td>Margin/Fee</td>
<td class="cost-value">${formatCostForExport(result.daily_margin_cost)}</td>
</tr>
<tr class="total-row">
<td>Total Daily</td>
<td class="cost-value">${formatCostForExport(result.daily_cost)}</td>
</tr>
</table>
` : ""}
${result.monthly_cost !== null ? `
<h2>Monthly Cost Estimate (${formatRequestsForExport(result.num_requests_per_month)} requests/month)</h2>
<table>
<tr>
<th>Cost Type</th>
<th>Amount</th>
</tr>
<tr>
<td>Input Cost</td>
<td class="cost-value">${formatCostForExport(result.monthly_input_cost)}</td>
</tr>
<tr>
<td>Output Cost</td>
<td class="cost-value">${formatCostForExport(result.monthly_output_cost)}</td>
</tr>
<tr>
<td>Margin/Fee</td>
<td class="cost-value">${formatCostForExport(result.monthly_margin_cost)}</td>
</tr>
<tr class="total-row">
<td>Total Monthly</td>
<td class="cost-value">${formatCostForExport(result.monthly_cost)}</td>
</tr>
</table>
` : ""}
${result.input_cost_per_token || result.output_cost_per_token ? `
<h2>Token Pricing</h2>
<table>
<tr>
<th>Token Type</th>
<th>Price per 1M Tokens</th>
</tr>
${result.input_cost_per_token ? `
<tr>
<td>Input Tokens</td>
<td class="cost-value">$${(result.input_cost_per_token * 1000000).toFixed(2)}</td>
</tr>
` : ""}
${result.output_cost_per_token ? `
<tr>
<td>Output Tokens</td>
<td class="cost-value">$${(result.output_cost_per_token * 1000000).toFixed(2)}</td>
</tr>
` : ""}
</table>
` : ""}
<div class="footer">
<p>Generated by LiteLLM Pricing Calculator on ${new Date().toLocaleString()}</p>
</div>
</body>
</html>
`;
printWindow.document.write(html);
printWindow.document.close();
printWindow.onload = () => {
printWindow.print();
};
};
export const exportToCSV = (result: CostEstimateResponse): void => {
const rows = [
["🚅 LiteLLM Cost Estimate Report"],
[""],
["Configuration"],
["Model", result.model],
["Provider", result.provider || "-"],
["Input Tokens per Request", result.input_tokens.toString()],
["Output Tokens per Request", result.output_tokens.toString()],
["Requests per Day", result.num_requests_per_day?.toString() || "-"],
["Requests per Month", result.num_requests_per_month?.toString() || "-"],
[""],
["Per-Request Costs"],
["Input Cost", result.input_cost_per_request.toString()],
["Output Cost", result.output_cost_per_request.toString()],
["Margin/Fee", result.margin_cost_per_request.toString()],
["Total per Request", result.cost_per_request.toString()],
];
if (result.daily_cost !== null) {
rows.push(
[""],
["Daily Costs"],
["Daily Input Cost", result.daily_input_cost?.toString() || "-"],
["Daily Output Cost", result.daily_output_cost?.toString() || "-"],
["Daily Margin/Fee", result.daily_margin_cost?.toString() || "-"],
["Total Daily", result.daily_cost.toString()]
);
}
if (result.monthly_cost !== null) {
rows.push(
[""],
["Monthly Costs"],
["Monthly Input Cost", result.monthly_input_cost?.toString() || "-"],
["Monthly Output Cost", result.monthly_output_cost?.toString() || "-"],
["Monthly Margin/Fee", result.monthly_margin_cost?.toString() || "-"],
["Total Monthly", result.monthly_cost.toString()]
);
}
if (result.input_cost_per_token || result.output_cost_per_token) {
rows.push(
[""],
["Token Pricing (per 1M tokens)"],
["Input Token Price", result.input_cost_per_token ? `$${(result.input_cost_per_token * 1000000).toFixed(2)}` : "-"],
["Output Token Price", result.output_cost_per_token ? `$${(result.output_cost_per_token * 1000000).toFixed(2)}` : "-"]
);
}
const csv = rows.map(row => row.join(",")).join("\n");
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `cost_estimate_${result.model.replace(/\//g, "_")}_${new Date().toISOString().split("T")[0]}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
};
@@ -1,104 +0,0 @@
import React from "react";
import { Form, InputNumber, Select, Row, Col } from "antd";
import { PricingFormValues } from "./types";
interface PricingFormProps {
models: string[];
onValuesChange: (changedValues: Partial<PricingFormValues>, allValues: PricingFormValues) => void;
}
const PricingForm: React.FC<PricingFormProps> = ({ models, onValuesChange }) => {
return (
<Form
layout="vertical"
onValuesChange={onValuesChange}
initialValues={{
input_tokens: 1000,
output_tokens: 500,
}}
>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="model"
label="Model"
rules={[{ required: true, message: "Please select a model" }]}
>
<Select
showSearch
placeholder="Select a model"
optionFilterProp="label"
filterOption={(input, option) =>
String(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
options={models.map((model) => ({
value: model,
label: model,
}))}
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
name="input_tokens"
label="Input Tokens (per request)"
rules={[{ required: true, message: "Required" }]}
>
<InputNumber
min={0}
style={{ width: "100%" }}
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
/>
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
name="output_tokens"
label="Output Tokens (per request)"
rules={[{ required: true, message: "Required" }]}
>
<InputNumber
min={0}
style={{ width: "100%" }}
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="num_requests_per_day"
label="Requests per Day"
tooltip="Optional: Enter expected daily request volume"
>
<InputNumber
min={0}
style={{ width: "100%" }}
placeholder="e.g., 1000"
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="num_requests_per_month"
label="Requests per Month"
tooltip="Optional: Enter expected monthly request volume"
>
<InputNumber
min={0}
style={{ width: "100%" }}
placeholder="e.g., 30000"
formatter={(value) => `${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}
/>
</Form.Item>
</Col>
</Row>
</Form>
);
};
export default PricingForm;
@@ -1,87 +0,0 @@
import { useState, useCallback, useRef, useEffect } from "react";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
import NotificationsManager from "../../molecules/notifications_manager";
import { CostEstimateRequest, CostEstimateResponse } from "../types";
import { PricingFormValues } from "./types";
const DEBOUNCE_MS = 500;
export function useCostEstimate(accessToken: string | null) {
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<CostEstimateResponse | null>(null);
const debounceRef = useRef<NodeJS.Timeout | null>(null);
const fetchEstimate = useCallback(
async (values: PricingFormValues) => {
if (!accessToken || !values.model) {
setResult(null);
return;
}
setLoading(true);
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl
? `${proxyBaseUrl}/cost/estimate`
: "/cost/estimate";
const requestBody: CostEstimateRequest = {
model: values.model,
input_tokens: values.input_tokens || 0,
output_tokens: values.output_tokens || 0,
num_requests_per_day: values.num_requests_per_day || null,
num_requests_per_month: values.num_requests_per_month || null,
};
const response = await fetch(url, {
method: "POST",
headers: {
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
if (response.ok) {
const data: CostEstimateResponse = await response.json();
setResult(data);
} else {
const errorData = await response.json();
const errorMessage =
errorData.detail?.error || errorData.detail || "Failed to estimate cost";
NotificationsManager.fromBackend(errorMessage);
setResult(null);
}
} catch (error) {
console.error("Error estimating cost:", error);
setResult(null);
} finally {
setLoading(false);
}
},
[accessToken]
);
const debouncedFetch = useCallback(
(values: PricingFormValues) => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
debounceRef.current = setTimeout(() => {
fetchEstimate(values);
}, DEBOUNCE_MS);
},
[fetchEstimate]
);
useEffect(() => {
return () => {
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
};
}, []);
return { loading, result, debouncedFetch };
}
@@ -1,137 +0,0 @@
import React, { useState, useEffect } from "react";
import {
Card,
Table,
TableHead,
TableRow,
TableHeaderCell,
TableCell,
TableBody,
Text,
Button,
} from "@tremor/react";
import { Icon } from "@tremor/react";
import { getBudgetSettings } from "../networking";
import { InputNumber } from "antd";
import {
TrashIcon,
} from "@heroicons/react/outline";
interface BudgetSettingsPageProps {
accessToken: string | null;
}
interface budgetSettingsItem {
field_name: string;
field_type: string;
field_value: any;
field_description: string;
}
const BudgetSettings: React.FC<BudgetSettingsPageProps> = ({ accessToken }) => {
const [budgetSettings, setBudgetSettings] = useState<budgetSettingsItem[]>([]);
useEffect(() => {
if (!accessToken) {
return;
}
getBudgetSettings(accessToken).then((data) => {
console.log("budget settings", data);
let budget_settings = data.budget_settings;
setBudgetSettings(budget_settings);
});
}, [accessToken]);
const handleInputChange = (fieldName: string, newValue: any) => {
// Update the value in the state
const updatedSettings = budgetSettings.map((setting) =>
setting.field_name === fieldName ? { ...setting, field_value: newValue } : setting,
);
setBudgetSettings(updatedSettings);
};
const handleUpdateField = (fieldName: string, idx: number) => {
if (!accessToken) {
return;
}
let fieldValue = budgetSettings[idx].field_value;
if (fieldValue == null || fieldValue == undefined) {
return;
}
try {
const updatedSettings = budgetSettings.map((setting) =>
setting.field_name === fieldName ? { ...setting, stored_in_db: true } : setting,
);
setBudgetSettings(updatedSettings);
} catch (error) {
// do something
}
};
const handleResetField = (fieldName: string, idx: number) => {
if (!accessToken) {
return;
}
try {
const updatedSettings = budgetSettings.map((setting) =>
setting.field_name === fieldName ? { ...setting, stored_in_db: null, field_value: null } : setting,
);
setBudgetSettings(updatedSettings);
} catch (error) {
// do something
}
};
return (
<div className="w-full mx-4">
<Card>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Setting</TableHeaderCell>
<TableHeaderCell>Value</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{budgetSettings.map((value, index) => (
<TableRow key={index}>
<TableCell>
<Text>{value.field_name}</Text>
<p
style={{
fontSize: "0.65rem",
color: "#808080",
fontStyle: "italic",
}}
className="mt-1"
>
{value.field_description}
</p>
</TableCell>
<TableCell>
{value.field_type == "Integer" ? (
<InputNumber
step={1}
value={value.field_value}
onChange={(newValue) => handleInputChange(value.field_name, newValue)} // Handle value change
/>
) : null}
</TableCell>
<TableCell>
<Button onClick={() => handleUpdateField(value.field_name, index)}>Update</Button>
<Icon icon={TrashIcon} color="red" onClick={() => handleResetField(value.field_name, index)}>
Reset
</Icon>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
</div>
);
};
export default BudgetSettings;
@@ -1,47 +0,0 @@
import React from "react";
import { Text } from "@tremor/react";
import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
interface PremiumMCPSelectorProps {
onChange: (value: { servers: string[]; accessGroups: string[] }) => void;
value: { servers: string[]; accessGroups: string[] };
accessToken: string;
placeholder?: string;
premiumUser?: boolean;
}
export function PremiumMCPSelector({
onChange,
value,
accessToken,
placeholder = "Select MCP servers",
premiumUser = false,
}: PremiumMCPSelectorProps) {
if (!premiumUser) {
return (
<div>
<div className="flex flex-wrap gap-2 mb-3">
<div className="inline-flex items-center px-3 py-1.5 rounded-lg bg-purple-50 border border-purple-200 text-purple-800 text-sm font-medium opacity-50">
premium-mcp-server-1
</div>
<div className="inline-flex items-center px-3 py-1.5 rounded-lg bg-purple-50 border border-purple-200 text-purple-800 text-sm font-medium opacity-50">
premium-mcp-server-2
</div>
</div>
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<Text className="text-sm text-yellow-800">
MCP server access control is a LiteLLM Enterprise feature. Get a trial key{" "}
<a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer" className="underline">
here
</a>
.
</Text>
</div>
</div>
);
}
return <MCPServerSelector onChange={onChange} value={value} accessToken={accessToken} placeholder={placeholder} />;
}
export default PremiumMCPSelector;
@@ -1,47 +0,0 @@
import React from "react";
import { Text } from "@tremor/react";
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector";
interface PremiumVectorStoreSelectorProps {
onChange: (values: string[]) => void;
value: string[];
accessToken: string;
placeholder?: string;
premiumUser?: boolean;
}
export function PremiumVectorStoreSelector({
onChange,
value,
accessToken,
placeholder = "Select vector stores",
premiumUser = false,
}: PremiumVectorStoreSelectorProps) {
if (!premiumUser) {
return (
<div>
<div className="flex flex-wrap gap-2 mb-3">
<div className="inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-50 border border-blue-200 text-blue-800 text-sm font-medium opacity-50">
premium-vector-store-1
</div>
<div className="inline-flex items-center px-3 py-1.5 rounded-lg bg-blue-50 border border-blue-200 text-blue-800 text-sm font-medium opacity-50">
premium-vector-store-2
</div>
</div>
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<Text className="text-sm text-yellow-800">
Vector store access control is a LiteLLM Enterprise feature. Get a trial key{" "}
<a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer" className="underline">
here
</a>
.
</Text>
</div>
</div>
);
}
return <VectorStoreSelector onChange={onChange} value={value} accessToken={accessToken} placeholder={placeholder} />;
}
export default PremiumVectorStoreSelector;
@@ -1,312 +0,0 @@
import React, { useState, useEffect } from "react";
import {
Card,
Title,
Text,
TextInput,
Button,
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
Grid,
Col,
Subtitle,
} from "@tremor/react";
import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking";
import NotificationsManager from "./molecules/notifications_manager";
interface CostTrackingSettingsProps {
userID: string | null;
userRole: string | null;
accessToken: string | null;
}
interface DiscountConfig {
[provider: string]: number;
}
const CostTrackingSettings: React.FC<CostTrackingSettingsProps> = ({
userID,
userRole,
accessToken
}) => {
const [discountConfig, setDiscountConfig] = useState<DiscountConfig>({});
const [newProvider, setNewProvider] = useState<string>("");
const [newDiscount, setNewDiscount] = useState<string>("");
const [loading, setLoading] = useState(false);
const [isFetching, setIsFetching] = useState(true);
useEffect(() => {
if (accessToken) {
fetchDiscountConfig();
}
}, [accessToken]);
const fetchDiscountConfig = async () => {
setIsFetching(true);
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl
? `${proxyBaseUrl}/config/cost_discount_config`
: "/config/cost_discount_config";
const response = await fetch(url, {
method: "GET",
headers: {
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (response.ok) {
const data = await response.json();
setDiscountConfig(data.values || {});
} else {
console.error("Failed to fetch discount config");
}
} catch (error) {
console.error("Error fetching discount config:", error);
NotificationsManager.fromBackend("Failed to fetch discount configuration");
} finally {
setIsFetching(false);
}
};
const handleSave = async () => {
setLoading(true);
try {
const proxyBaseUrl = getProxyBaseUrl();
const url = proxyBaseUrl
? `${proxyBaseUrl}/config/cost_discount_config`
: "/config/cost_discount_config";
const response = await fetch(url, {
method: "PATCH",
headers: {
[getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(discountConfig),
});
if (response.ok) {
NotificationsManager.success("Cost discount configuration updated successfully");
await fetchDiscountConfig();
} else {
const errorData = await response.json();
const errorMessage = errorData.detail?.error || errorData.detail || "Failed to update settings";
NotificationsManager.fromBackend(errorMessage);
}
} catch (error) {
console.error("Error updating discount config:", error);
NotificationsManager.fromBackend("Failed to update discount configuration");
} finally {
setLoading(false);
}
};
const handleAddProvider = () => {
if (!newProvider || !newDiscount) {
NotificationsManager.fromBackend("Please enter both provider and discount value");
return;
}
const discountValue = parseFloat(newDiscount);
if (isNaN(discountValue) || discountValue < 0 || discountValue > 1) {
NotificationsManager.fromBackend("Discount must be between 0 and 1 (0% to 100%)");
return;
}
setDiscountConfig(prev => ({
...prev,
[newProvider.trim()]: discountValue,
}));
setNewProvider("");
setNewDiscount("");
};
const handleRemoveProvider = (provider: string) => {
setDiscountConfig(prev => {
const updated = { ...prev };
delete updated[provider];
return updated;
});
};
const handleDiscountChange = (provider: string, value: string) => {
const discountValue = parseFloat(value);
if (!isNaN(discountValue) && discountValue >= 0 && discountValue <= 1) {
setDiscountConfig(prev => ({
...prev,
[provider]: discountValue,
}));
}
};
if (!accessToken) {
return null;
}
const hasChanges = Object.keys(discountConfig).length > 0;
return (
<div style={{ width: "100%" }} className="relative">
<div className="mb-6">
<Title>Cost Tracking Settings</Title>
<Subtitle>
Configure cost discounts for different LLM providers. Discounts are applied as multipliers.
</Subtitle>
</div>
<Grid numItems={1} className="gap-6">
<Col>
<Card>
<div className="flex justify-between items-start mb-4">
<div>
<Title>Provider Discounts</Title>
<Text className="mt-1 text-sm text-gray-500">
Set custom discount rates per provider (e.g., 0.05 = 5% discount)
</Text>
</div>
<Button
onClick={handleSave}
loading={loading}
disabled={loading || isFetching}
size="sm"
>
Save Changes
</Button>
</div>
{isFetching ? (
<div className="py-8 text-center">
<Text className="text-gray-500">Loading configuration...</Text>
</div>
) : (
<>
{Object.keys(discountConfig).length > 0 ? (
<div className="mt-4">
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Provider</TableHeaderCell>
<TableHeaderCell>Discount Value</TableHeaderCell>
<TableHeaderCell>Percentage</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{Object.entries(discountConfig)
.sort(([a], [b]) => a.localeCompare(b))
.map(([provider, discount]) => (
<TableRow key={provider}>
<TableCell className="font-medium">{provider}</TableCell>
<TableCell>
<TextInput
value={discount.toString()}
onValueChange={(value) => handleDiscountChange(provider, value)}
placeholder="0.05"
className="w-32"
/>
</TableCell>
<TableCell>
<span className="text-gray-700 font-medium">
{(discount * 100).toFixed(1)}%
</span>
</TableCell>
<TableCell>
<Button
size="xs"
variant="secondary"
color="red"
onClick={() => handleRemoveProvider(provider)}
>
Remove
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="py-8 text-center border border-dashed border-gray-300 rounded-lg">
<Text className="text-gray-500">
No provider discounts configured. Add your first provider below.
</Text>
</div>
)}
<div className="border-t pt-6 mt-6">
<div className="mb-3">
<Text className="font-medium text-gray-900">Add Provider Discount</Text>
<Text className="text-xs text-gray-500 mt-1">
Common providers: vertex_ai, gemini, openai, anthropic, openrouter, bedrock, azure
</Text>
</div>
<Grid numItems={3} className="gap-3">
<Col numColSpan={1}>
<TextInput
placeholder="Provider name"
value={newProvider}
onValueChange={setNewProvider}
/>
</Col>
<Col numColSpan={1}>
<TextInput
placeholder="Discount (0.05 for 5%)"
value={newDiscount}
onValueChange={setNewDiscount}
/>
</Col>
<Col numColSpan={1}>
<Button
onClick={handleAddProvider}
className="w-full"
disabled={!newProvider || !newDiscount}
>
Add Provider
</Button>
</Col>
</Grid>
</div>
</>
)}
</Card>
</Col>
<Col>
<Card>
<Title>How It Works</Title>
<div className="mt-4 space-y-3">
<div>
<Text className="font-medium text-gray-900">Cost Calculation</Text>
<Text className="text-sm text-gray-600 mt-1">
Discounts are applied to provider costs: <code className="bg-gray-100 px-1 py-0.5 rounded">final_cost = base_cost × (1 - discount)</code>
</Text>
</div>
<div>
<Text className="font-medium text-gray-900">Example</Text>
<Text className="text-sm text-gray-600 mt-1">
A 5% discount (0.05) on a $10.00 request results in: $10.00 × (1 - 0.05) = $9.50
</Text>
</div>
<div>
<Text className="font-medium text-gray-900">Valid Range</Text>
<Text className="text-sm text-gray-600 mt-1">
Discount values must be between 0 (0%) and 1 (100%)
</Text>
</div>
</div>
</Card>
</Col>
</Grid>
</div>
);
};
export default CostTrackingSettings;
@@ -1,318 +0,0 @@
import React, { useState, useCallback } from "react";
import { Card, Title, Text, Table, TableHead, TableRow, TableHeaderCell, TableCell, TableBody } from "@tremor/react";
import { Input } from "antd";
import { ChevronDownIcon, ChevronRightIcon, PlusCircleIcon } from "@heroicons/react/outline";
import NotificationManager from "./molecules/notifications_manager";
interface KeyValueItem {
id?: string;
key: string;
value: string;
}
interface GenericKeyValueManagerProps {
title: string;
description: string;
keyLabel: string;
valueLabel: string;
keyPlaceholder: string;
valuePlaceholder: string;
items: KeyValueItem[];
onItemsChange: (items: KeyValueItem[]) => void;
onSave?: () => Promise<void>;
showSaveButton?: boolean;
isCollapsible?: boolean;
defaultExpanded?: boolean;
configExample?: React.ReactNode;
additionalActions?: (item: KeyValueItem) => React.ReactNode;
}
const GenericKeyValueManager: React.FC<GenericKeyValueManagerProps> = ({
title,
description,
keyLabel,
valueLabel,
keyPlaceholder,
valuePlaceholder,
items,
onItemsChange,
onSave,
showSaveButton = true,
isCollapsible = false,
defaultExpanded = true,
configExample,
additionalActions,
}) => {
const [newKey, setNewKey] = useState<string>("");
const [newValue, setNewValue] = useState<string>("");
const [editingItem, setEditingItem] = useState<KeyValueItem | null>(null);
const [editingKey, setEditingKey] = useState<string>("");
const [editingValue, setEditingValue] = useState<string>("");
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const generateId = () => Math.random().toString(36).substr(2, 9);
const handleAddItem = useCallback(() => {
if (newKey.trim() && newValue.trim()) {
const newItem: KeyValueItem = {
id: generateId(),
key: newKey.trim(),
value: newValue.trim(),
};
onItemsChange([...items, newItem]);
setNewKey("");
setNewValue("");
} else {
NotificationManager.fromBackend(`Please provide both ${keyLabel.toLowerCase()} and ${valueLabel.toLowerCase()}`);
}
}, [newKey, newValue, items, onItemsChange, keyLabel, valueLabel]);
const handleEditItem = useCallback((item: KeyValueItem) => {
setEditingItem({ ...item });
setEditingKey(item.key);
setEditingValue(item.value);
}, []);
const handleSaveEdit = useCallback(() => {
if (editingKey.trim() && editingValue.trim()) {
const updatedItems = items.map((item) =>
item.id === editingItem?.id ? { ...item, key: editingKey.trim(), value: editingValue.trim() } : item,
);
onItemsChange(updatedItems);
setEditingItem(null);
setEditingKey("");
setEditingValue("");
} else {
NotificationManager.fromBackend(`Please provide both ${keyLabel.toLowerCase()} and ${valueLabel.toLowerCase()}`);
}
}, [editingKey, editingValue, items, editingItem, onItemsChange, keyLabel, valueLabel]);
const handleCancelEdit = useCallback(() => {
setEditingItem(null);
setEditingKey("");
setEditingValue("");
}, []);
const handleDeleteItem = useCallback(
(id: string) => {
const updatedItems = items.filter((item) => item.id !== id);
onItemsChange(updatedItems);
},
[items, onItemsChange],
);
const handleSave = useCallback(async () => {
if (onSave) {
try {
await onSave();
} catch (error) {
console.error("Failed to save:", error);
}
}
}, [onSave]);
const ContentSection = useCallback(
() => (
<div className="space-y-6">
{/* Add New Item Section */}
<Card>
<Title className="mb-4">Add New {keyLabel}</Title>
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-xs text-gray-500 mb-1">{keyLabel}</label>
<Input
value={newKey}
onChange={(e) => setNewKey(e.target.value)}
placeholder={keyPlaceholder}
size="middle"
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">{valueLabel}</label>
<Input
value={newValue}
onChange={(e) => setNewValue(e.target.value)}
placeholder={valuePlaceholder}
size="middle"
/>
</div>
<div className="flex items-end">
<button
onClick={handleAddItem}
disabled={!newKey.trim() || !newValue.trim()}
className={`flex items-center px-4 py-2 rounded-md text-sm ${
!newKey.trim() || !newValue.trim()
? "bg-gray-300 text-gray-500 cursor-not-allowed"
: "bg-green-600 text-white hover:bg-green-700"
}`}
>
<PlusCircleIcon className="w-4 h-4 mr-1" />
Add {keyLabel}
</button>
</div>
</div>
</Card>
{/* Manage Existing Items Section */}
<Card>
<div className="flex justify-between items-center mb-4">
<Title>Manage Existing {keyLabel}s</Title>
{showSaveButton && (
<button onClick={handleSave} className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700">
Save All Changes
</button>
)}
</div>
<div className="rounded-lg custom-border relative">
<div className="overflow-x-auto">
<Table className="[&_td]:py-0.5 [&_th]:py-1">
<TableHead>
<TableRow>
<TableHeaderCell className="py-1 h-8">{keyLabel}</TableHeaderCell>
<TableHeaderCell className="py-1 h-8">{valueLabel}</TableHeaderCell>
<TableHeaderCell className="py-1 h-8">Actions</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{items.map((item) => (
<TableRow key={item.id} className="h-8">
{editingItem && editingItem.id === item.id ? (
<>
<TableCell className="py-0.5">
<Input value={editingKey} onChange={(e) => setEditingKey(e.target.value)} size="small" />
</TableCell>
<TableCell className="py-0.5">
<Input
value={editingValue}
onChange={(e) => setEditingValue(e.target.value)}
size="small"
/>
</TableCell>
<TableCell className="py-0.5 whitespace-nowrap">
<div className="flex space-x-2">
<button
onClick={handleSaveEdit}
className="text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded hover:bg-blue-100"
>
Save
</button>
<button
onClick={handleCancelEdit}
className="text-xs bg-gray-50 text-gray-600 px-2 py-1 rounded hover:bg-gray-100"
>
Cancel
</button>
</div>
</TableCell>
</>
) : (
<>
<TableCell className="py-0.5 text-sm text-gray-900">{item.key}</TableCell>
<TableCell className="py-0.5 text-sm text-gray-500">{item.value}</TableCell>
<TableCell className="py-0.5 whitespace-nowrap">
<div className="flex space-x-2">
{additionalActions && additionalActions(item)}
<button
onClick={() => handleEditItem(item)}
className="text-xs bg-blue-50 text-blue-600 px-2 py-1 rounded hover:bg-blue-100"
>
Edit
</button>
<button
onClick={() => handleDeleteItem(item.id!)}
className="text-xs bg-red-50 text-red-600 px-2 py-1 rounded hover:bg-red-100"
>
Delete
</button>
</div>
</TableCell>
</>
)}
</TableRow>
))}
{items.length === 0 && (
<TableRow>
<TableCell colSpan={3} className="py-0.5 text-sm text-gray-500 text-center">
No {keyLabel.toLowerCase()}s added yet. Add a new {keyLabel.toLowerCase()} above.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
</Card>
{/* Configuration Example */}
{configExample && (
<Card>
<Title className="mb-4">Configuration Example</Title>
{configExample}
</Card>
)}
</div>
),
[
keyLabel,
valueLabel,
keyPlaceholder,
valuePlaceholder,
newKey,
newValue,
items,
editingItem,
editingKey,
editingValue,
showSaveButton,
configExample,
additionalActions,
handleAddItem,
handleSave,
handleEditItem,
handleSaveEdit,
handleCancelEdit,
handleDeleteItem,
],
);
if (isCollapsible) {
return (
<Card className="mb-6">
<div className="flex items-center justify-between cursor-pointer" onClick={() => setIsExpanded(!isExpanded)}>
<div className="flex flex-col">
<Title className="mb-0">{title}</Title>
<p className="text-sm text-gray-500">{description}</p>
</div>
<div className="flex items-center">
{isExpanded ? (
<ChevronDownIcon className="w-5 h-5 text-gray-500" />
) : (
<ChevronRightIcon className="w-5 h-5 text-gray-500" />
)}
</div>
</div>
{isExpanded && (
<div className="mt-4">
<ContentSection />
</div>
)}
</Card>
);
}
return (
<div>
<div className="mb-6">
<Title>{title}</Title>
<Text className="text-gray-600 mt-2 block">{description}</Text>
</div>
<div>
<ContentSection />
</div>
</div>
);
};
export default GenericKeyValueManager;
@@ -1,243 +0,0 @@
import React, { useState } from "react";
import { Typography, Badge, Card, Checkbox, Select, Slider, Tooltip, Divider, Row, Col } from "antd";
import {
AzureTextModerationConfigurationProps,
AZURE_TEXT_MODERATION_CATEGORIES,
SEVERITY_LEVELS,
} from "./azure_text_moderation_types";
import { InfoCircleOutlined, SafetyOutlined } from "@ant-design/icons";
const { Title, Text, Paragraph } = Typography;
const { Option } = Select;
/**
* A reusable component for configuring Azure Text Moderation guardrail settings
* Allows configuration of categories, global severity threshold, and per-category thresholds
*/
const AzureTextModerationConfiguration: React.FC<AzureTextModerationConfigurationProps> = ({
selectedCategories,
globalSeverityThreshold,
categorySpecificThresholds,
onCategorySelect,
onGlobalSeverityChange,
onCategorySeverityChange,
}) => {
const [showAdvanced, setShowAdvanced] = useState(false);
const getSeverityLabel = (value: number) => {
const level = SEVERITY_LEVELS.find((l) => l.value === value);
return level ? level.label : `Level ${value}`;
};
const getSeverityColor = (value: number) => {
if (value === 0) return "#52c41a"; // green
if (value === 2) return "#faad14"; // orange
if (value === 4) return "#fa8c16"; // dark orange
return "#f5222d"; // red
};
const handleSelectAll = () => {
AZURE_TEXT_MODERATION_CATEGORIES.forEach((category) => {
if (!selectedCategories.includes(category.name)) {
onCategorySelect(category.name);
}
});
};
const handleUnselectAll = () => {
selectedCategories.forEach((category) => {
onCategorySelect(category);
});
};
return (
<div className="azure-text-moderation-configuration">
<div className="flex justify-between items-center mb-5">
<div className="flex items-center">
<SafetyOutlined className="text-blue-600 mr-2 text-lg" />
<Title level={4} className="mb-0 font-semibold text-gray-800">
Azure Text Moderation
</Title>
</div>
<Badge
count={selectedCategories.length}
showZero
style={{ backgroundColor: selectedCategories.length > 0 ? "#1890ff" : "#d9d9d9" }}
overflowCount={999}
>
<Text className="text-gray-500">{selectedCategories.length} categories selected</Text>
</Badge>
</div>
<Card className="mb-6">
<div className="flex justify-between items-center mb-4">
<Title level={5} className="mb-0">
Content Categories
</Title>
<div className="space-x-2">
<a onClick={handleSelectAll} className="text-blue-600 hover:text-blue-800 cursor-pointer">
Select All
</a>
<span className="text-gray-300">|</span>
<a
onClick={handleUnselectAll}
className="text-red-600 hover:text-red-800 cursor-pointer"
style={{ display: selectedCategories.length > 0 ? "inline" : "none" }}
>
Unselect All
</a>
</div>
</div>
<Paragraph className="text-gray-600 mb-4">Select which content categories to monitor and filter</Paragraph>
<Row gutter={[16, 16]}>
{AZURE_TEXT_MODERATION_CATEGORIES.map((category) => (
<Col xs={24} sm={12} key={category.name}>
<Card
size="small"
className={`cursor-pointer transition-all duration-200 ${
selectedCategories.includes(category.name)
? "border-blue-500 bg-blue-50 shadow-sm"
: "border-gray-200 hover:border-gray-300"
}`}
onClick={() => onCategorySelect(category.name)}
>
<div className="flex items-start">
<Checkbox
checked={selectedCategories.includes(category.name)}
onChange={() => onCategorySelect(category.name)}
className="mr-3 mt-1"
/>
<div className="flex-1">
<Text strong className="text-sm">
{category.name}
</Text>
<br />
<Text className="text-xs text-gray-600">{category.description}</Text>
</div>
</div>
</Card>
</Col>
))}
</Row>
</Card>
<Card className="mb-6">
<div className="flex items-center mb-4">
<Title level={5} className="mb-0 mr-2">
Global Severity Threshold
</Title>
<Tooltip title="Content with severity levels at or above this threshold will be flagged">
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</div>
<Paragraph className="text-gray-600 mb-4">
Set the minimum severity level that will trigger the guardrail for all selected categories
</Paragraph>
<div className="mb-4">
<Row>
<Col span={16}>
<Slider
min={0}
max={6}
step={2}
value={globalSeverityThreshold}
onChange={onGlobalSeverityChange}
marks={{
0: { label: "Safe", style: { color: "#52c41a" } },
2: { label: "Low", style: { color: "#faad14" } },
4: { label: "Medium", style: { color: "#fa8c16" } },
6: { label: "High", style: { color: "#f5222d" } },
}}
tooltip={{
formatter: (value) => getSeverityLabel(value || 0),
}}
/>
</Col>
<Col span={8} className="pl-4">
<div className="text-center">
<div
className="inline-block px-3 py-1 rounded-full text-white font-medium"
style={{ backgroundColor: getSeverityColor(globalSeverityThreshold) }}
>
{getSeverityLabel(globalSeverityThreshold)}
</div>
</div>
</Col>
</Row>
</div>
</Card>
<Card>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center">
<Title level={5} className="mb-0 mr-2">
Per-Category Thresholds
</Title>
<Tooltip title="Override the global threshold for specific categories">
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</div>
<a
onClick={() => setShowAdvanced(!showAdvanced)}
className="text-blue-600 hover:text-blue-800 cursor-pointer"
>
{showAdvanced ? "Hide Advanced" : "Show Advanced"}
</a>
</div>
{showAdvanced && (
<>
<Paragraph className="text-gray-600 mb-4">
Set custom severity thresholds for individual categories. Leave empty to use the global threshold.
</Paragraph>
{selectedCategories.length === 0 ? (
<div className="text-center py-8 text-gray-500">
Please select at least one category to configure per-category thresholds
</div>
) : (
<div className="space-y-4">
{selectedCategories.map((category) => (
<div key={category}>
<div className="flex justify-between items-center mb-2">
<Text strong>{category}</Text>
<Select
placeholder="Use global threshold"
style={{ width: 200 }}
value={categorySpecificThresholds[category]}
onChange={(value) => onCategorySeverityChange(category, value)}
allowClear
>
{SEVERITY_LEVELS.map((level) => (
<Option key={level.value} value={level.value}>
<div className="flex items-center justify-between">
<span>{level.label}</span>
<div
className="w-3 h-3 rounded-full ml-2"
style={{ backgroundColor: getSeverityColor(level.value) }}
/>
</div>
</Option>
))}
</Select>
</div>
<Text className="text-xs text-gray-500">
{AZURE_TEXT_MODERATION_CATEGORIES.find((c) => c.name === category)?.description}
</Text>
{category !== selectedCategories[selectedCategories.length - 1] && <Divider className="my-4" />}
</div>
))}
</div>
)}
</>
)}
</Card>
</div>
);
};
export default AzureTextModerationConfiguration;
@@ -1,92 +0,0 @@
import React, { useState } from "react";
import { Button, Space, Card } from "antd";
import AzureTextModerationConfiguration from "./azure_text_moderation_configuration";
import NotificationsManager from "../molecules/notifications_manager";
/**
* Example component showing how to use the AzureTextModerationConfiguration
* This demonstrates the state management and event handling required
*/
const AzureTextModerationExample: React.FC = () => {
const [selectedCategories, setSelectedCategories] = useState<string[]>(["Hate", "Violence"]);
const [globalSeverityThreshold, setGlobalSeverityThreshold] = useState<number>(2);
const [categorySpecificThresholds, setCategorySpecificThresholds] = useState<{ [key: string]: number }>({
Hate: 4,
});
const handleCategorySelect = (category: string) => {
setSelectedCategories((prev) =>
prev.includes(category) ? prev.filter((c) => c !== category) : [...prev, category],
);
};
const handleGlobalSeverityChange = (threshold: number) => {
setGlobalSeverityThreshold(threshold);
};
const handleCategorySeverityChange = (category: string, threshold: number) => {
setCategorySpecificThresholds((prev) => ({
...prev,
[category]: threshold,
}));
};
const handleSave = () => {
// Example of how to construct the configuration object
const config = {
categories: selectedCategories,
severity_threshold: globalSeverityThreshold,
severity_threshold_by_category: categorySpecificThresholds,
};
console.log("Azure Text Moderation Configuration:", config);
NotificationsManager.success("Configuration saved successfully!");
};
const handleReset = () => {
setSelectedCategories(["Hate", "Violence"]);
setGlobalSeverityThreshold(2);
setCategorySpecificThresholds({ Hate: 4 });
NotificationsManager.info("Configuration reset to defaults");
};
return (
<div style={{ maxWidth: 800, margin: "0 auto", padding: 20 }}>
<Card title="Azure Text Moderation Configuration Example" className="mb-6">
<AzureTextModerationConfiguration
selectedCategories={selectedCategories}
globalSeverityThreshold={globalSeverityThreshold}
categorySpecificThresholds={categorySpecificThresholds}
onCategorySelect={handleCategorySelect}
onGlobalSeverityChange={handleGlobalSeverityChange}
onCategorySeverityChange={handleCategorySeverityChange}
/>
<div className="mt-6 pt-4 border-t">
<Space>
<Button type="primary" onClick={handleSave}>
Save Configuration
</Button>
<Button onClick={handleReset}>Reset to Defaults</Button>
</Space>
</div>
</Card>
<Card title="Current Configuration" size="small">
<pre style={{ fontSize: 12, backgroundColor: "#f5f5f5", padding: 12, borderRadius: 4 }}>
{JSON.stringify(
{
categories: selectedCategories,
severity_threshold: globalSeverityThreshold,
severity_threshold_by_category: categorySpecificThresholds,
},
null,
2,
)}
</pre>
</Card>
</div>
);
};
export default AzureTextModerationExample;
@@ -1,74 +0,0 @@
export interface AzureTextModerationCategory {
name: string;
description: string;
enabled: boolean;
severityThreshold?: number;
}
export interface AzureTextModerationConfigurationProps {
selectedCategories: string[];
globalSeverityThreshold: number;
categorySpecificThresholds: { [key: string]: number };
onCategorySelect: (category: string) => void;
onGlobalSeverityChange: (threshold: number) => void;
onCategorySeverityChange: (category: string, threshold: number) => void;
}
export interface AzureTextModerationGuardrail {
guardrail_id: string;
guardrail_name: string | null;
litellm_params: {
guardrail: string;
mode: string;
default_on: boolean;
categories?: string[];
severity_threshold?: number;
severity_threshold_by_category?: { [key: string]: number };
[key: string]: any;
};
guardrail_info: Record<string, any> | null;
created_at?: string;
updated_at?: string;
}
export const AZURE_TEXT_MODERATION_CATEGORIES = [
{
name: "Hate",
description: "Content that attacks or uses discriminatory language based on protected characteristics",
},
{
name: "Sexual",
description: "Content that describes sexual activity or other sexual content",
},
{
name: "SelfHarm",
description: "Content that promotes, encourages, or depicts acts of self-harm",
},
{
name: "Violence",
description: "Content that depicts death, violence, or physical injury",
},
];
export const SEVERITY_LEVELS = [
{
value: 0,
label: "Level 0 - Safe",
description: "Content is appropriate and safe",
},
{
value: 2,
label: "Level 2 - Low",
description: "Content may be inappropriate in some contexts",
},
{
value: 4,
label: "Level 4 - Medium",
description: "Content is inappropriate and should be filtered",
},
{
value: 6,
label: "Level 6 - High",
description: "Content is harmful and should be blocked",
},
];
@@ -1,49 +0,0 @@
/**
* Type definitions for Content Filter Configuration
*/
export interface PrebuiltPattern {
name: string;
display_name: string;
category: string;
description: string;
}
export interface Pattern {
id: string;
type: "prebuilt" | "custom";
name: string;
display_name?: string;
pattern?: string;
action: "BLOCK" | "MASK";
}
export interface BlockedWord {
id: string;
keyword: string;
action: "BLOCK" | "MASK";
description?: string;
}
export interface ContentFilterSettings {
prebuilt_patterns: PrebuiltPattern[];
pattern_categories: string[];
supported_actions: string[];
}
export interface ContentFilterConfigurationProps {
prebuiltPatterns: PrebuiltPattern[];
categories: string[];
selectedPatterns: Pattern[];
blockedWords: BlockedWord[];
blockedWordsFile?: string;
onPatternAdd: (pattern: Pattern) => void;
onPatternRemove: (id: string) => void;
onPatternActionChange: (id: string, action: "BLOCK" | "MASK") => void;
onBlockedWordAdd: (word: BlockedWord) => void;
onBlockedWordRemove: (id: string) => void;
onBlockedWordUpdate: (id: string, field: string, value: any) => void;
onFileUpload: (content: string) => void;
accessToken: string | null;
}
@@ -1,188 +0,0 @@
import React, { useRef, useState } from "react";
import { Input, Tabs, Typography } from "antd";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import { CodeOutlined, EyeOutlined } from "@ant-design/icons";
const { TextArea } = Input;
const { Text } = Typography;
interface CustomCodeEditorProps {
value: string;
onChange: (value: string) => void;
height?: string;
placeholder?: string;
disabled?: boolean;
}
const CustomCodeEditor: React.FC<CustomCodeEditorProps> = ({
value,
onChange,
height = "350px",
placeholder = `def apply_guardrail(inputs, request_data, input_type):
# inputs: contains texts, images, tools, tool_calls, structured_messages, model
# request_data: contains model, user_id, team_id, end_user_id, metadata
# input_type: "request" or "response"
for text in inputs["texts"]:
# Example: Block if SSN pattern is detected
if regex_match(text, r"\\d{3}-\\d{2}-\\d{4}"):
return block("SSN detected in message")
return allow()`,
disabled = false,
}) => {
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [activeTab, setActiveTab] = useState<string>("edit");
const [cursorPosition, setCursorPosition] = useState({ line: 1, column: 1 });
// Calculate cursor position
const updateCursorPosition = () => {
if (textareaRef.current) {
const textarea = textareaRef.current;
const textBeforeCursor = value.substring(0, textarea.selectionStart);
const lines = textBeforeCursor.split("\n");
const line = lines.length;
const column = lines[lines.length - 1].length + 1;
setCursorPosition({ line, column });
}
};
// Handle tab key for indentation
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Tab") {
e.preventDefault();
const textarea = e.currentTarget;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
// Insert 4 spaces at cursor position
const newValue = value.substring(0, start) + " " + value.substring(end);
onChange(newValue);
// Move cursor after the inserted spaces
setTimeout(() => {
textarea.selectionStart = textarea.selectionEnd = start + 4;
}, 0);
}
};
const lineCount = value.split("\n").length;
const tabItems = [
{
key: "edit",
label: (
<span className="flex items-center gap-1.5">
<CodeOutlined />
Edit
</span>
),
children: (
<div className="relative" style={{ height }}>
{/* Line numbers */}
<div
className="absolute left-0 top-0 bottom-0 w-12 bg-[#1e1e1e] border-r border-[#3c3c3c] text-right pr-2 pt-3 overflow-hidden select-none"
style={{ fontFamily: "monospace", fontSize: "13px", lineHeight: "1.5" }}
>
{Array.from({ length: Math.max(lineCount, 15) }, (_, i) => (
<div key={i + 1} className="text-gray-500 h-[19.5px]">
{i + 1}
</div>
))}
</div>
{/* Code editor */}
<textarea
ref={textareaRef as any}
value={value}
onChange={(e) => {
onChange(e.target.value);
updateCursorPosition();
}}
onKeyDown={handleKeyDown}
onClick={updateCursorPosition}
onKeyUp={updateCursorPosition}
placeholder={placeholder}
disabled={disabled}
spellCheck={false}
className="w-full h-full pl-14 pr-4 pt-3 pb-3 font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-blue-500"
style={{
backgroundColor: "#1e1e1e",
color: "#d4d4d4",
border: "1px solid #3c3c3c",
borderRadius: "8px",
lineHeight: "1.5",
tabSize: 4,
}}
/>
{/* Status bar */}
<div className="absolute bottom-0 left-0 right-0 h-6 bg-[#252526] border-t border-[#3c3c3c] flex items-center justify-between px-3 text-xs text-gray-400 rounded-b-lg">
<span>Python-like (Sandboxed)</span>
<span>Ln {cursorPosition.line}, Col {cursorPosition.column}</span>
</div>
</div>
),
},
{
key: "preview",
label: (
<span className="flex items-center gap-1.5">
<EyeOutlined />
Preview
</span>
),
children: (
<div style={{ height }} className="overflow-auto rounded-lg border border-gray-200">
<SyntaxHighlighter
language="python"
style={vscDarkPlus}
showLineNumbers
wrapLines
customStyle={{
margin: 0,
borderRadius: "8px",
fontSize: "13px",
minHeight: height,
}}
lineNumberStyle={{
minWidth: "3em",
paddingRight: "1em",
color: "#6e7681",
borderRight: "1px solid #3c3c3c",
marginRight: "1em",
}}
>
{value || placeholder}
</SyntaxHighlighter>
</div>
),
},
];
return (
<div className="custom-code-editor">
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={tabItems}
className="custom-code-tabs"
size="small"
/>
<style>{`
.custom-code-tabs .ant-tabs-nav {
margin-bottom: 8px;
}
.custom-code-tabs .ant-tabs-tab {
padding: 4px 12px;
}
.custom-code-editor textarea::placeholder {
color: #6e7681;
}
`}</style>
</div>
);
};
export default CustomCodeEditor;
@@ -1,588 +0,0 @@
import React, { useState, useCallback } from "react";
import { Card, Text, Button } from "@tremor/react";
import { Collapse, Typography, Tooltip, Spin, Alert, Tabs } from "antd";
import {
PlayCircleOutlined,
InfoCircleOutlined,
CodeOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
EditOutlined,
BookOutlined,
ExperimentOutlined,
} from "@ant-design/icons";
import NotificationsManager from "../../molecules/notifications_manager";
import { testCustomCodeGuardrail } from "../../networking";
import CustomCodeEditor from "./CustomCodeEditor";
import { CUSTOM_CODE_PRIMITIVES, CUSTOM_CODE_EXAMPLES, DEFAULT_CUSTOM_CODE } from "./custom_code_constants";
const { Panel } = Collapse;
const { Paragraph } = Typography;
interface CustomCodePlaygroundProps {
accessToken: string | null;
initialCode?: string;
onCodeChange?: (code: string) => void;
showTestingPanel?: boolean;
}
interface TestResult {
action: "allow" | "block" | "modify";
reason?: string;
modified_texts?: string[];
modified_images?: string[];
modified_tool_calls?: any[];
execution_time_ms?: number;
error?: string;
}
const CustomCodePlayground: React.FC<CustomCodePlaygroundProps> = ({
accessToken,
initialCode = DEFAULT_CUSTOM_CODE,
onCodeChange,
showTestingPanel = true,
}) => {
const [customCode, setCustomCode] = useState(initialCode);
const [testInput, setTestInput] = useState(
JSON.stringify(
{
texts: ["Hello, my SSN is 123-45-6789"],
images: [],
tools: [],
tool_calls: [],
structured_messages: [
{ role: "user", content: "Hello, my SSN is 123-45-6789" },
],
model: "gpt-4",
},
null,
2
)
);
const [requestData, setRequestData] = useState(
JSON.stringify(
{
model: "gpt-4",
user_id: "test-user",
team_id: "test-team",
end_user_id: "end-user-123",
metadata: {},
},
null,
2
)
);
const [inputType, setInputType] = useState<"request" | "response">("request");
const [testResult, setTestResult] = useState<TestResult | null>(null);
const [isTesting, setIsTesting] = useState(false);
const [activeTab, setActiveTab] = useState<string>("editor");
const handleCodeChange = useCallback(
(code: string) => {
setCustomCode(code);
onCodeChange?.(code);
},
[onCodeChange]
);
const handleRunTest = async () => {
if (!accessToken) {
NotificationsManager.fromBackend("No access token available");
return;
}
setIsTesting(true);
setTestResult(null);
try {
let parsedInputs: any;
let parsedRequestData: any;
try {
parsedInputs = JSON.parse(testInput);
} catch (e) {
throw new Error("Invalid JSON in test input");
}
try {
parsedRequestData = JSON.parse(requestData);
} catch (e) {
throw new Error("Invalid JSON in request data");
}
const response = await testCustomCodeGuardrail(accessToken, {
custom_code: customCode,
test_input: parsedInputs,
input_type: inputType,
request_data: parsedRequestData,
});
if (response.success && response.result) {
setTestResult(response.result);
if (response.result.action === "allow") {
NotificationsManager.success("Guardrail allowed the request");
} else if (response.result.action === "block") {
NotificationsManager.fromBackend(`Guardrail blocked: ${response.result.reason || "No reason provided"}`);
} else if (response.result.action === "modify") {
NotificationsManager.success("Guardrail modified the content");
}
} else if (response.error) {
setTestResult({
action: "block",
error: response.error,
});
NotificationsManager.fromBackend(`Test failed: ${response.error}`);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
setTestResult({
action: "block",
error: errorMessage,
});
NotificationsManager.fromBackend(`Test failed: ${errorMessage}`);
} finally {
setIsTesting(false);
}
};
const loadExample = (exampleKey: keyof typeof CUSTOM_CODE_EXAMPLES) => {
setCustomCode(CUSTOM_CODE_EXAMPLES[exampleKey]);
onCodeChange?.(CUSTOM_CODE_EXAMPLES[exampleKey]);
setActiveTab("editor");
};
const renderTestResult = () => {
if (!testResult) return null;
const isError = !!testResult.error;
const isAllow = testResult.action === "allow";
const isBlock = testResult.action === "block";
const isModify = testResult.action === "modify";
return (
<div className="mt-4">
<Text className="font-medium text-gray-700 block mb-2">Test Result</Text>
<div
className={`rounded-lg p-4 border ${
isError
? "bg-red-50 border-red-200"
: isAllow
? "bg-green-50 border-green-200"
: isBlock
? "bg-orange-50 border-orange-200"
: "bg-blue-50 border-blue-200"
}`}
>
<div className="flex items-center gap-2 mb-2">
{isError ? (
<CloseCircleOutlined className="text-red-500 text-lg" />
) : isAllow ? (
<CheckCircleOutlined className="text-green-500 text-lg" />
) : isBlock ? (
<CloseCircleOutlined className="text-orange-500 text-lg" />
) : (
<EditOutlined className="text-blue-500 text-lg" />
)}
<span
className={`font-semibold ${
isError
? "text-red-700"
: isAllow
? "text-green-700"
: isBlock
? "text-orange-700"
: "text-blue-700"
}`}
>
{isError ? "Error" : testResult.action.toUpperCase()}
</span>
{testResult.execution_time_ms && (
<span className="text-xs text-gray-500 ml-auto">
{testResult.execution_time_ms.toFixed(2)}ms
</span>
)}
</div>
{isError && (
<Alert type="error" message={testResult.error} className="mt-2" />
)}
{isBlock && testResult.reason && (
<Paragraph className="text-orange-700 mb-0 mt-2">
<strong>Reason:</strong> {testResult.reason}
</Paragraph>
)}
{isModify && testResult.modified_texts && testResult.modified_texts.length > 0 && (
<div className="mt-2">
<Text className="font-medium text-blue-700 block mb-1">Modified Texts:</Text>
<pre className="bg-white rounded p-2 text-xs overflow-auto max-h-32 border border-blue-100">
{JSON.stringify(testResult.modified_texts, null, 2)}
</pre>
</div>
)}
</div>
</div>
);
};
const renderPrimitivesReference = () => (
<div className="space-y-4">
{Object.entries(CUSTOM_CODE_PRIMITIVES).map(([category, primitives]) => (
<div key={category}>
<Text className="font-semibold text-gray-700 block mb-2">{category}</Text>
<div className="bg-gray-50 rounded-lg p-3">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-1 pr-4 font-medium text-gray-600">Function</th>
<th className="text-left py-1 font-medium text-gray-600">Description</th>
</tr>
</thead>
<tbody>
{primitives.map((primitive) => (
<tr key={primitive.name} className="border-b border-gray-100 last:border-0">
<td className="py-1.5 pr-4">
<code className="text-xs bg-blue-50 text-blue-700 px-1.5 py-0.5 rounded font-mono">
{primitive.signature}
</code>
</td>
<td className="py-1.5 text-gray-600">{primitive.description}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
<div>
<Text className="font-semibold text-gray-700 block mb-2">Return Values</Text>
<div className="bg-gray-50 rounded-lg p-3">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-1 pr-4 font-medium text-gray-600">Function</th>
<th className="text-left py-1 font-medium text-gray-600">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-gray-100">
<td className="py-1.5 pr-4"><code className="text-xs bg-green-100 text-green-700 px-1.5 py-0.5 rounded font-mono">allow()</code></td>
<td className="py-1.5 text-gray-600">Let request/response through</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-1.5 pr-4"><code className="text-xs bg-red-100 text-red-700 px-1.5 py-0.5 rounded font-mono">block(reason)</code></td>
<td className="py-1.5 text-gray-600">Reject with message</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-1.5 pr-4"><code className="text-xs bg-yellow-100 text-yellow-700 px-1.5 py-0.5 rounded font-mono">modify(texts=[], images=[], tool_calls=[])</code></td>
<td className="py-1.5 text-gray-600">Transform content</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
);
const renderInputParamsReference = () => (
<div className="space-y-4">
<div>
<Text className="font-semibold text-gray-700 block mb-2">`inputs` Parameter</Text>
<div className="bg-gray-50 rounded-lg p-3">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-1 pr-4 font-medium text-gray-600">Field</th>
<th className="text-left py-1 pr-4 font-medium text-gray-600">Type</th>
<th className="text-left py-1 font-medium text-gray-600">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-gray-100">
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">texts</code></td>
<td className="py-1.5 pr-4 text-gray-500">List[str]</td>
<td className="py-1.5 text-gray-600">Extracted text from the request/response</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">images</code></td>
<td className="py-1.5 pr-4 text-gray-500">List[str]</td>
<td className="py-1.5 text-gray-600">Extracted images (for image guardrails)</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">tools</code></td>
<td className="py-1.5 pr-4 text-gray-500">List[dict]</td>
<td className="py-1.5 text-gray-600">Tools sent to the LLM</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">tool_calls</code></td>
<td className="py-1.5 pr-4 text-gray-500">List[dict]</td>
<td className="py-1.5 text-gray-600">Tool calls returned from the LLM</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">structured_messages</code></td>
<td className="py-1.5 pr-4 text-gray-500">List[dict]</td>
<td className="py-1.5 text-gray-600">Full messages with role info</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">model</code></td>
<td className="py-1.5 pr-4 text-gray-500">str</td>
<td className="py-1.5 text-gray-600">The model being used</td>
</tr>
</tbody>
</table>
</div>
</div>
<div>
<Text className="font-semibold text-gray-700 block mb-2">`request_data` Parameter</Text>
<div className="bg-gray-50 rounded-lg p-3">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-200">
<th className="text-left py-1 pr-4 font-medium text-gray-600">Field</th>
<th className="text-left py-1 pr-4 font-medium text-gray-600">Type</th>
<th className="text-left py-1 font-medium text-gray-600">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-gray-100">
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">model</code></td>
<td className="py-1.5 pr-4 text-gray-500">str</td>
<td className="py-1.5 text-gray-600">Model name</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">user_id</code></td>
<td className="py-1.5 pr-4 text-gray-500">str</td>
<td className="py-1.5 text-gray-600">User ID from API key</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">team_id</code></td>
<td className="py-1.5 pr-4 text-gray-500">str</td>
<td className="py-1.5 text-gray-600">Team ID from API key</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">end_user_id</code></td>
<td className="py-1.5 pr-4 text-gray-500">str</td>
<td className="py-1.5 text-gray-600">End user ID</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-1.5 pr-4"><code className="text-xs bg-gray-200 px-1 rounded font-mono">metadata</code></td>
<td className="py-1.5 pr-4 text-gray-500">dict</td>
<td className="py-1.5 text-gray-600">Request metadata</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
);
const renderExamplesTab = () => (
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<button
onClick={() => loadExample("blockSSN")}
className="text-left p-4 border border-gray-200 rounded-lg hover:border-blue-400 hover:bg-blue-50 transition-colors"
>
<Text className="font-medium text-gray-800 block mb-1">🔒 Block PII (SSN)</Text>
<Text className="text-xs text-gray-500">Detect and block Social Security Numbers</Text>
</button>
<button
onClick={() => loadExample("redactEmail")}
className="text-left p-4 border border-gray-200 rounded-lg hover:border-blue-400 hover:bg-blue-50 transition-colors"
>
<Text className="font-medium text-gray-800 block mb-1">📧 Redact Emails</Text>
<Text className="text-xs text-gray-500">Replace email addresses with [EMAIL REDACTED]</Text>
</button>
<button
onClick={() => loadExample("blockSQL")}
className="text-left p-4 border border-gray-200 rounded-lg hover:border-blue-400 hover:bg-blue-50 transition-colors"
>
<Text className="font-medium text-gray-800 block mb-1">🛡 Block SQL Injection</Text>
<Text className="text-xs text-gray-500">Prevent SQL code in requests</Text>
</button>
<button
onClick={() => loadExample("validateJSON")}
className="text-left p-4 border border-gray-200 rounded-lg hover:border-blue-400 hover:bg-blue-50 transition-colors"
>
<Text className="font-medium text-gray-800 block mb-1"> Validate JSON Response</Text>
<Text className="text-xs text-gray-500">Ensure responses have required fields</Text>
</button>
<button
onClick={() => loadExample("checkURLs")}
className="text-left p-4 border border-gray-200 rounded-lg hover:border-blue-400 hover:bg-blue-50 transition-colors"
>
<Text className="font-medium text-gray-800 block mb-1">🔗 Check URLs</Text>
<Text className="text-xs text-gray-500">Validate all URLs in responses</Text>
</button>
<button
onClick={() => loadExample("combined")}
className="text-left p-4 border border-gray-200 rounded-lg hover:border-blue-400 hover:bg-blue-50 transition-colors"
>
<Text className="font-medium text-gray-800 block mb-1">🔄 Combined Checks</Text>
<Text className="text-xs text-gray-500">Multiple checks with redaction and blocking</Text>
</button>
</div>
);
const tabItems = [
{
key: "editor",
label: (
<span className="flex items-center gap-1.5">
<CodeOutlined />
Code Editor
</span>
),
children: (
<div className="space-y-4">
<CustomCodeEditor value={customCode} onChange={handleCodeChange} height="350px" />
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg text-sm text-yellow-800">
<strong> Sandbox Restrictions:</strong> No imports, no file I/O, no network access, no exec() or eval().
Only LiteLLM-provided primitives are available.
</div>
</div>
),
},
{
key: "examples",
label: (
<span className="flex items-center gap-1.5">
<BookOutlined />
Examples
</span>
),
children: renderExamplesTab(),
},
{
key: "primitives",
label: (
<span className="flex items-center gap-1.5">
<InfoCircleOutlined />
Primitives Reference
</span>
),
children: renderPrimitivesReference(),
},
{
key: "params",
label: (
<span className="flex items-center gap-1.5">
<InfoCircleOutlined />
Input Parameters
</span>
),
children: renderInputParamsReference(),
},
];
if (showTestingPanel) {
tabItems.push({
key: "test",
label: (
<span className="flex items-center gap-1.5">
<ExperimentOutlined />
Test
</span>
),
children: (
<div className="space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div>
<div className="flex items-center justify-between mb-2">
<Text className="font-medium text-gray-700">Test Input (inputs parameter)</Text>
<Tooltip title="This represents the 'inputs' parameter passed to your apply_guardrail function">
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</div>
<textarea
value={testInput}
onChange={(e) => setTestInput(e.target.value)}
className="w-full h-48 p-3 font-mono text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter test input JSON..."
/>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<Text className="font-medium text-gray-700">Request Data (request_data parameter)</Text>
<Tooltip title="This represents the 'request_data' parameter passed to your apply_guardrail function">
<InfoCircleOutlined className="text-gray-400" />
</Tooltip>
</div>
<textarea
value={requestData}
onChange={(e) => setRequestData(e.target.value)}
className="w-full h-48 p-3 font-mono text-sm border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="Enter request data JSON..."
/>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Text className="text-sm text-gray-600">Input Type:</Text>
<select
value={inputType}
onChange={(e) => setInputType(e.target.value as "request" | "response")}
className="border border-gray-200 rounded px-3 py-1.5 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
>
<option value="request">request</option>
<option value="response">response</option>
</select>
</div>
<Button
onClick={handleRunTest}
disabled={!accessToken || isTesting}
icon={isTesting ? undefined : PlayCircleOutlined}
className="ml-auto"
>
{isTesting ? (
<span className="flex items-center gap-2">
<Spin size="small" /> Running Test...
</span>
) : (
"Run Test"
)}
</Button>
</div>
{renderTestResult()}
</div>
),
});
}
return (
<Card className="p-0 overflow-hidden">
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={tabItems}
className="custom-code-playground-tabs"
tabBarStyle={{ padding: "0 16px", marginBottom: 0 }}
/>
<div className="p-4">
{tabItems.find(tab => tab.key === activeTab)?.children}
</div>
<style>{`
.custom-code-playground-tabs .ant-tabs-nav {
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.custom-code-playground-tabs .ant-tabs-tab {
padding: 12px 16px;
}
.custom-code-playground-tabs .ant-tabs-tab-active {
background: white;
}
`}</style>
</Card>
);
};
export default CustomCodePlayground;
@@ -1,194 +0,0 @@
// Custom Code Guardrail Constants
export const DEFAULT_CUSTOM_CODE = `def apply_guardrail(inputs, request_data, input_type):
# inputs: contains texts, images, tools, tool_calls, structured_messages, model
# request_data: contains model, user_id, team_id, end_user_id, metadata
# input_type: "request" or "response"
for text in inputs["texts"]:
# Example: Block if SSN pattern is detected
if regex_match(text, r"\\d{3}-\\d{2}-\\d{4}"):
return block("SSN detected in message")
return allow()
`;
export const CUSTOM_CODE_PRIMITIVES = {
"Regex Functions": [
{
name: "regex_match",
signature: "regex_match(text, pattern)",
description: "Returns True if pattern found in text",
},
{
name: "regex_replace",
signature: "regex_replace(text, pattern, replacement)",
description: "Replace all matches of pattern with replacement",
},
{
name: "regex_find_all",
signature: "regex_find_all(text, pattern)",
description: "Return list of all matches",
},
],
"JSON Functions": [
{
name: "json_parse",
signature: "json_parse(text)",
description: "Parse JSON string, returns None on error",
},
{
name: "json_stringify",
signature: "json_stringify(obj)",
description: "Convert object to JSON string",
},
{
name: "json_schema_valid",
signature: "json_schema_valid(obj, schema)",
description: "Validate object against JSON schema",
},
],
"URL Functions": [
{
name: "extract_urls",
signature: "extract_urls(text)",
description: "Extract all URLs from text",
},
{
name: "is_valid_url",
signature: "is_valid_url(url)",
description: "Check if URL is valid",
},
{
name: "all_urls_valid",
signature: "all_urls_valid(text)",
description: "Check all URLs in text are valid",
},
],
"Code Detection": [
{
name: "detect_code",
signature: "detect_code(text)",
description: "Returns True if code detected",
},
{
name: "detect_code_languages",
signature: "detect_code_languages(text)",
description: "Returns list of detected languages",
},
{
name: "contains_code_language",
signature: 'contains_code_language(text, ["sql", "python"])',
description: "Check for specific languages",
},
],
"Text Utilities": [
{
name: "contains",
signature: "contains(text, substring)",
description: "Check if substring exists in text",
},
{
name: "contains_any",
signature: "contains_any(text, [substr1, substr2])",
description: "Check if any substring exists",
},
{
name: "word_count",
signature: "word_count(text)",
description: "Count words in text",
},
{
name: "char_count",
signature: "char_count(text)",
description: "Count characters in text",
},
{
name: "lower",
signature: "lower(text)",
description: "Convert text to lowercase",
},
{
name: "upper",
signature: "upper(text)",
description: "Convert text to uppercase",
},
{
name: "trim",
signature: "trim(text)",
description: "Remove leading/trailing whitespace",
},
],
};
export const CUSTOM_CODE_EXAMPLES = {
blockSSN: `def apply_guardrail(inputs, request_data, input_type):
for text in inputs["texts"]:
if regex_match(text, r"\\d{3}-\\d{2}-\\d{4}"):
return block("SSN detected")
return allow()
`,
redactEmail: `def apply_guardrail(inputs, request_data, input_type):
pattern = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"
modified = []
for text in inputs["texts"]:
modified.append(regex_replace(text, pattern, "[EMAIL REDACTED]"))
return modify(texts=modified)
`,
blockSQL: `def apply_guardrail(inputs, request_data, input_type):
if input_type != "request":
return allow()
for text in inputs["texts"]:
if contains_code_language(text, ["sql"]):
return block("SQL code not allowed")
return allow()
`,
validateJSON: `def apply_guardrail(inputs, request_data, input_type):
if input_type != "response":
return allow()
schema = {
"type": "object",
"required": ["name", "value"]
}
for text in inputs["texts"]:
obj = json_parse(text)
if obj is None:
return block("Invalid JSON response")
if not json_schema_valid(obj, schema):
return block("Response missing required fields")
return allow()
`,
checkURLs: `def apply_guardrail(inputs, request_data, input_type):
if input_type != "response":
return allow()
for text in inputs["texts"]:
if not all_urls_valid(text):
return block("Response contains invalid URLs")
return allow()
`,
combined: `def apply_guardrail(inputs, request_data, input_type):
modified = []
for text in inputs["texts"]:
# Redact SSN
text = regex_replace(text, r"\\d{3}-\\d{2}-\\d{4}", "[SSN]")
# Redact credit cards
text = regex_replace(text, r"\\d{16}", "[CARD]")
modified.append(text)
# Block SQL in requests
if input_type == "request":
for text in inputs["texts"]:
if contains_code_language(text, ["sql"]):
return block("SQL injection blocked")
return modify(texts=modified)
`,
};
@@ -1,184 +0,0 @@
import React, { useState, useEffect } from "react";
import { Form, Select, Spin } from "antd";
import { TextInput } from "@tremor/react";
import {
guardrail_provider_map,
populateGuardrailProviders,
populateGuardrailProviderMap,
} from "./guardrail_info_helpers";
import { getGuardrailProviderSpecificParams } from "../networking";
import NumericalInput from "../shared/numerical_input";
interface GuardrailProviderSpecificFieldsProps {
selectedProvider: string;
accessToken?: string | null;
providerParams?: ProviderParamsResponse | null;
}
interface ProviderParam {
param: string;
description: string;
required: boolean;
default_value?: string;
options?: string[];
type?: string;
fields?: { [key: string]: ProviderParam };
dict_key_options?: string[];
dict_value_type?: string;
}
interface ProviderParamsResponse {
[provider: string]: { [key: string]: ProviderParam };
}
const GuardrailProviderSpecificFields: React.FC<GuardrailProviderSpecificFieldsProps> = ({
selectedProvider,
accessToken,
providerParams: providerParamsProp = null,
}) => {
const [loading, setLoading] = useState(false);
const [providerParams, setProviderParams] = useState<ProviderParamsResponse | null>(providerParamsProp);
const [error, setError] = useState<string | null>(null);
// Fetch provider-specific parameters when component mounts
useEffect(() => {
if (providerParamsProp) {
setProviderParams(providerParamsProp);
return;
}
const fetchProviderParams = async () => {
if (!accessToken) return;
setLoading(true);
setError(null);
try {
const data = await getGuardrailProviderSpecificParams(accessToken);
setProviderParams(data);
// Populate dynamic providers from API response
populateGuardrailProviders(data);
populateGuardrailProviderMap(data);
} catch (error) {
console.error("Error fetching provider params:", error);
setError("Failed to load provider parameters");
} finally {
setLoading(false);
}
};
if (!providerParamsProp) {
fetchProviderParams();
}
}, [accessToken, providerParamsProp]);
// If no provider is selected, don't render anything
if (!selectedProvider) {
return null;
}
// Show loading state
if (loading) {
return <Spin tip="Loading provider parameters..." />;
}
// Show error state
if (error) {
return <div className="text-red-500">{error}</div>;
}
// Get the provider key matching the selected provider
const providerKey = guardrail_provider_map[selectedProvider]?.toLowerCase();
// Get parameters for the selected provider
const providerFields = providerParams && providerParams[providerKey];
if (!providerFields || Object.keys(providerFields).length === 0) {
return null;
}
// Render fields function
const renderFields = (fields: { [key: string]: ProviderParam }, parentKey = "") => {
return Object.entries(fields).map(([fieldKey, field]) => {
const fullFieldKey = parentKey ? `${parentKey}.${fieldKey}` : fieldKey;
// Handle nested fields
if (field.type === "nested" && field.fields) {
return (
<div key={fullFieldKey}>
<div className="mb-2 font-medium">{fieldKey}</div>
<div className="ml-4 border-l-2 border-gray-200 pl-4">{renderFields(field.fields, fullFieldKey)}</div>
</div>
);
}
return (
<Form.Item
key={fullFieldKey}
name={fullFieldKey}
label={fieldKey}
tooltip={field.description}
rules={field.required ? [{ required: true, message: `${fieldKey} is required` }] : undefined}
>
{field.type === "select" && field.options ? (
<Select placeholder={field.description} defaultValue={field.default_value}>
{field.options.map((option) => (
<Select.Option key={option} value={option}>
{option}
</Select.Option>
))}
</Select>
) : field.type === "multiselect" && field.options ? (
<Select mode="multiple" placeholder={field.description} defaultValue={field.default_value}>
{field.options.map((option) => (
<Select.Option key={option} value={option}>
{option}
</Select.Option>
))}
</Select>
) : field.type === "dict" && field.dict_key_options ? (
<div className="space-y-3">
<div className="text-sm text-gray-600 mb-3">{field.description}</div>
{field.dict_key_options.map((key) => (
<div key={key} className="flex items-center space-x-3">
<div className="w-24 font-medium text-sm">{key}</div>
<div className="flex-1">
<Form.Item name={[fullFieldKey, key]} style={{ marginBottom: 0 }}>
{field.dict_value_type === "number" ? (
<NumericalInput step={1} width={200} placeholder={`Enter ${key} value`} />
) : field.dict_value_type === "boolean" ? (
<Select placeholder={`Select ${key} value`}>
<Select.Option value={true}>True</Select.Option>
<Select.Option value={false}>False</Select.Option>
</Select>
) : (
<TextInput placeholder={`Enter ${key} value`} type="text" />
)}
</Form.Item>
</div>
</div>
))}
</div>
) : field.type === "bool" || field.type === "boolean" ? (
<Select placeholder={field.description} defaultValue={field.default_value}>
<Select.Option value="true">True</Select.Option>
<Select.Option value="false">False</Select.Option>
</Select>
) : field.type === "number" ? (
<NumericalInput step={1} width={400} placeholder={field.description} />
) : fieldKey.includes("password") || fieldKey.includes("secret") || fieldKey.includes("key") ? (
<TextInput placeholder={field.description} type="password" />
) : (
<TextInput placeholder={field.description} type="text" />
)}
</Form.Item>
);
});
};
return <>{renderFields(providerFields)}</>;
};
export default GuardrailProviderSpecificFields;
@@ -1,4 +0,0 @@
// PII Configuration exports
export { default as PiiConfiguration } from "./pii_configuration";
export * from "./pii_components";
export * from "./types";
@@ -1,90 +0,0 @@
import React from "react";
const codeString = `import asyncio
from openai import AsyncOpenAI
from openai.types.chat import ChatCompletionUserMessageParam
from mcp import ClientSession
from mcp.client.sse import sse_client
from litellm.experimental_mcp_client.tools import (
transform_mcp_tool_to_openai_tool,
transform_openai_tool_call_request_to_mcp_tool_call_request,
)
async def main():
# Initialize clients
client = AsyncOpenAI(
api_key="sk-1234",
base_url="http://localhost:4000"
)
# Connect to MCP
async with sse_client("http://localhost:4000/mcp/") as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
mcp_tools = await session.list_tools()
print("List of MCP tools for MCP server:", mcp_tools.tools)
# Create message
messages = [
ChatCompletionUserMessageParam(
content="Send an email about LiteLLM supporting MCP",
role="user"
)
]
# Request with tools
response = await client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=[transform_mcp_tool_to_openai_tool(tool) for tool in mcp_tools.tools],
tool_choice="auto"
)
# Handle tool call
if response.choices[0].message.tool_calls:
tool_call = response.choices[0].message.tool_calls[0]
if tool_call:
# Convert format
mcp_call = transform_openai_tool_call_request_to_mcp_tool_call_request(
openai_tool=tool_call.model_dump()
)
# Execute tool
result = await session.call_tool(
name=mcp_call.name,
arguments=mcp_call.arguments
)
print("Result:", result)
# Run it
asyncio.run(main())`;
export const CodeExample: React.FC = () => {
return (
<div className="bg-white rounded-lg shadow h-full">
<div className="border-b px-4 py-3">
<h3 className="text-base font-medium text-gray-900">Using MCP Tools</h3>
</div>
<div className="p-4">
<div className="flex items-center gap-2 mb-2">
<div className="flex-1">
<div className="text-sm font-medium text-gray-700">Python integration</div>
</div>
<button
className="text-xs bg-gray-100 hover:bg-gray-200 text-gray-600 px-2 py-1 rounded-md transition-colors"
onClick={() => {
navigator.clipboard.writeText(codeString);
}}
>
Copy
</button>
</div>
<div className="overflow-auto rounded-md bg-gray-50 border" style={{ maxHeight: "calc(100vh - 280px)" }}>
<pre className="p-3 text-xs font-mono text-gray-800 whitespace-pre overflow-x-auto">{codeString}</pre>
</div>
</div>
</div>
);
};
@@ -1,42 +0,0 @@
import React from "react";
import { Form } from "antd";
import { TextInput } from "@tremor/react";
interface Field {
field_name: string;
field_type: string;
field_description: string;
field_value: string;
}
interface DynamicFieldsProps {
fields: Field[];
selectedProvider: string;
}
const getPlaceholder = (provider: string) => {
// Implement your placeholder logic based on the provider
return `Enter your ${provider} value here`;
};
const DynamicFields: React.FC<DynamicFieldsProps> = ({ fields, selectedProvider }) => {
if (fields.length === 0) return null;
return (
<>
{fields.map((field) => (
<Form.Item
key={field.field_name}
rules={[{ required: true, message: "Required" }]}
label={field.field_name.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase())}
name={field.field_name}
tooltip={field.field_description}
className="mb-2"
>
<TextInput placeholder={field.field_value} type="password" />
</Form.Item>
))}
</>
);
};
export default DynamicFields;
@@ -1,31 +0,0 @@
import React from "react";
import { LineChart } from "@tremor/react";
interface TimeToFirstTokenProps {
modelMetrics: any[];
modelMetricsCategories: string[];
customTooltip: any;
premiumUser: boolean;
}
const TimeToFirstToken: React.FC<TimeToFirstTokenProps> = ({
modelMetrics,
modelMetricsCategories,
customTooltip,
premiumUser,
}) => {
return (
<LineChart
title="Time to First token (s)"
className="h-72"
data={modelMetrics}
index="date"
showLegend={false}
categories={modelMetricsCategories}
colors={["indigo", "rose"]}
connectNulls={true}
customTooltip={customTooltip}
/>
);
};
export default TimeToFirstToken;
@@ -1,22 +0,0 @@
export interface EditModalProps {
visible: boolean;
onCancel: () => void;
entity: Organization;
onSubmit: (entity: Organization) => void;
}
export interface OrganizationMember {
user_id: string;
user_role: string;
}
export interface Organization {
organization_id: string;
organization_name: string;
spend: number;
max_budget: number | null;
models: string[];
tpm_limit: number | null;
rpm_limit: number | null;
members: OrganizationMember[] | null;
}
@@ -1,93 +0,0 @@
import { TokenUsage } from "../chat_ui/ResponseMetrics";
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 StreamingChoices {
finish_reason?: string | null;
index: number;
delta: Delta;
logprobs?: any;
}
export interface Delta {
content?: string;
reasoning_content?: string;
role?: string;
function_call?: any;
tool_calls?: any;
audio?: any;
refusal?: any;
provider_specific_fields?: any;
}
export interface Usage {
completion_tokens: number;
prompt_tokens: number;
total_tokens: number;
completion_tokens_details?: {
accepted_prediction_tokens?: number;
audio_tokens?: number;
reasoning_tokens?: number;
rejected_prediction_tokens?: number;
text_tokens?: number | null;
};
prompt_tokens_details?: {
audio_tokens?: number;
cached_tokens?: number;
text_tokens?: number;
image_tokens?: number;
};
}
export interface StreamProcessCallbacks {
onContent: (content: string, model?: string) => void;
onReasoningContent: (content: string) => void;
onUsage?: (usage: TokenUsage) => void;
}
export const processStreamingResponse = (response: StreamingResponse, callbacks: StreamProcessCallbacks) => {
// Extract model information if available
const model = response.model;
// Process regular content
if (response.choices && response.choices.length > 0) {
const choice = response.choices[0];
if (choice.delta?.content) {
callbacks.onContent(choice.delta.content, model);
}
// Process reasoning content if it exists
if (choice.delta?.reasoning_content) {
callbacks.onReasoningContent(choice.delta.reasoning_content);
}
}
// Process usage information if it exists and we have a handler
if (response.usage && callbacks.onUsage) {
console.log("Processing usage data:", response.usage);
const usageData: TokenUsage = {
completionTokens: response.usage.completion_tokens,
promptTokens: response.usage.prompt_tokens,
totalTokens: response.usage.total_tokens,
};
// Extract reasoning tokens if available
if (response.usage.completion_tokens_details?.reasoning_tokens) {
usageData.reasoningTokens = response.usage.completion_tokens_details.reasoning_tokens;
}
callbacks.onUsage(usageData);
}
};
@@ -1,5 +0,0 @@
export { default as PromptTable } from "./prompt_table";
export { default as PromptInfoView } from "./prompt_info";
export { default as PromptEditorView } from "./prompt_editor_view";
export { default as ToolModal } from "./tool_modal";
export { default as VariableTextArea } from "./variable_textarea";
@@ -1,196 +0,0 @@
import React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Text } from "@tremor/react";
import { EyeIcon, CogIcon } from "@heroicons/react/outline";
import { Tag } from "antd";
interface ModelGroupInfo {
model_group: string;
providers: string[];
max_input_tokens?: number;
max_output_tokens?: number;
input_cost_per_token?: number;
output_cost_per_token?: number;
mode?: string;
tpm?: number;
rpm?: number;
supports_parallel_function_calling: boolean;
supports_vision: boolean;
supports_function_calling: boolean;
supported_openai_params?: string[];
[key: string]: any;
}
const formatCost = (cost: number) => {
return `$${(cost * 1_000_000).toFixed(4)}`;
};
const formatTokens = (tokens: number | undefined) => {
if (!tokens) return "N/A";
if (tokens >= 1000) {
return `${(tokens / 1000).toFixed(0)}K`;
}
return tokens.toString();
};
const formatLimits = (rpm?: number, tpm?: number) => {
const limits = [];
if (rpm) limits.push(`RPM: ${rpm.toLocaleString()}`);
if (tpm) limits.push(`TPM: ${tpm.toLocaleString()}`);
return limits.length > 0 ? limits.join(", ") : "N/A";
};
export const publicModelHubColumns = (): ColumnDef<ModelGroupInfo>[] => [
{
header: "#",
id: "index",
enableSorting: false,
cell: ({ row }) => {
const index = row.index + 1;
return <Text className="text-center">{index}</Text>;
},
size: 50,
},
{
header: "Model Group",
accessorKey: "model_group",
enableSorting: true,
cell: ({ row }) => <Text className="font-medium">{row.original.model_group}</Text>,
size: 150,
},
{
header: "Providers",
accessorKey: "providers",
enableSorting: true,
cell: ({ row }) => {
const providers = row.original.providers;
const getProviderColor = (provider: string) => {
switch (provider.toLowerCase()) {
case "openai":
return "green";
case "anthropic":
return "orange";
case "cohere":
return "blue";
default:
return "gray";
}
};
return (
<div className="flex flex-wrap gap-1">
{providers.map((provider) => (
<Tag key={provider} color={getProviderColor(provider)} className="text-xs">
{provider}
</Tag>
))}
</div>
);
},
size: 120,
},
{
header: "Mode",
accessorKey: "mode",
enableSorting: true,
cell: ({ row }) => {
const mode = row.original.mode;
const getModeIcon = (mode: string) => {
switch (mode?.toLowerCase()) {
case "chat":
return "💬";
case "rerank":
return "🔄";
case "embedding":
return "📄";
default:
return "🤖";
}
};
return (
<div className="flex items-center space-x-2">
<span>{getModeIcon(mode || "")}</span>
<Text>{mode || "Chat"}</Text>
</div>
);
},
size: 100,
},
{
header: "Max Input",
accessorKey: "max_input_tokens",
enableSorting: true,
cell: ({ row }) => <Text className="text-center">{formatTokens(row.original.max_input_tokens)}</Text>,
size: 100,
},
{
header: "Max Output",
accessorKey: "max_output_tokens",
enableSorting: true,
cell: ({ row }) => <Text className="text-center">{formatTokens(row.original.max_output_tokens)}</Text>,
size: 100,
},
{
header: "Input $/1K",
accessorKey: "input_cost_per_token",
enableSorting: true,
cell: ({ row }) => {
const cost = row.original.input_cost_per_token;
return <Text className="text-center">{cost ? formatCost(cost) : "Free"}</Text>;
},
size: 100,
},
{
header: "Output $/1K",
accessorKey: "output_cost_per_token",
enableSorting: true,
cell: ({ row }) => {
const cost = row.original.output_cost_per_token;
return <Text className="text-center">{cost ? formatCost(cost) : "Free"}</Text>;
},
size: 100,
},
{
header: "Features",
accessorKey: "supports_vision",
enableSorting: false,
cell: ({ row }) => {
const model = row.original;
const features = [];
if (model.supports_vision) {
features.push(
<div key="vision" className="flex items-center space-x-1" title="Vision">
<EyeIcon className="w-4 h-4 text-blue-600" />
</div>,
);
}
if (model.supports_function_calling || model.supports_parallel_function_calling) {
features.push(
<div key="functions" className="flex items-center space-x-1" title="Functions">
<CogIcon className="w-4 h-4 text-green-600" />
</div>,
);
}
return features.length > 0 ? (
<div className="flex space-x-2">{features}</div>
) : (
<Text className="text-gray-400">-</Text>
);
},
size: 100,
},
{
header: "Limits",
accessorKey: "rpm",
enableSorting: true,
cell: ({ row }) => {
const model = row.original;
return <Text className="text-xs text-gray-600">{formatLimits(model.rpm, model.tpm)}</Text>;
},
size: 150,
},
];
@@ -1,97 +0,0 @@
"use client";
import React, { useState } from "react";
import { Modal, Form, Input, Select } from "antd";
import { Button } from "@tremor/react";
import { userRequestModelCall } from "./networking";
import NotificationsManager from "./molecules/notifications_manager";
const { Option } = Select;
interface RequestAccessProps {
userModels: string[];
accessToken: string;
userID: string;
}
function onRequestAccess(formData: Record<string, any>): void {
// This function does nothing for now
}
const RequestAccess: React.FC<RequestAccessProps> = ({ userModels, accessToken, userID }) => {
const [form] = Form.useForm();
const [isModalVisible, setIsModalVisible] = useState(false);
const handleOk = () => {
setIsModalVisible(false);
form.resetFields();
};
const handleCancel = () => {
setIsModalVisible(false);
form.resetFields();
};
const handleRequestAccess = async (formValues: Record<string, any>) => {
try {
NotificationsManager.info("Requesting access");
// Extract form values
const { selectedModel, accessReason } = formValues;
// Call userRequestModelCall
const response = await userRequestModelCall(
accessToken, // You need to have accessToken available
selectedModel,
userID, // You need to have UserID available
accessReason,
);
onRequestAccess(formValues);
setIsModalVisible(true);
} catch (error) {
console.error("Error requesting access:", error);
}
};
return (
<div>
<Button size="xs" onClick={() => setIsModalVisible(true)}>
Request Access
</Button>
<Modal
title="Request Access"
open={isModalVisible}
width={800}
footer={null}
onOk={handleOk}
onCancel={handleCancel}
>
<Form
form={form}
onFinish={handleRequestAccess}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
labelAlign="left"
>
<Form.Item label="Select Model" name="selectedModel">
<Select placeholder="Select model" style={{ width: "100%" }}>
{userModels.map((model) => (
<Option key={model} value={model}>
{model}
</Option>
))}
</Select>
</Form.Item>
<Form.Item label="Reason for Access" name="accessReason">
<Input.TextArea rows={4} placeholder="Enter reason for access" />
</Form.Item>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button>Request Access</Button>
</div>
</Form>
</Modal>
</div>
);
};
export default RequestAccess;
@@ -1,68 +0,0 @@
"use client";
import { Setter } from "@/types";
import React from "react";
import { VirtualKeysTable } from "../VirtualKeysPage/VirtualKeysTable";
import useKeyList, { KeyResponse, Team } from "../key_team_helpers/key_list";
import { Organization } from "../networking";
// Define the props type
interface ViewKeyTableProps {
userID: string | null;
userRole: string | null;
accessToken: string | null;
selectedTeam: Team | null;
setSelectedTeam: React.Dispatch<React.SetStateAction<any | null>>;
data: KeyResponse[] | null;
setData: (keys: KeyResponse[]) => void;
teams: Team[] | null;
premiumUser: boolean;
currentOrg: Organization | null;
organizations: Organization[] | null;
setCurrentOrg: React.Dispatch<React.SetStateAction<Organization | null>>;
selectedKeyAlias: string | null;
setSelectedKeyAlias: Setter<string | null>;
createClicked: boolean;
setAccessToken?: (token: string) => void;
}
const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
accessToken,
selectedTeam,
setSelectedTeam,
teams,
currentOrg,
organizations,
setCurrentOrg,
selectedKeyAlias,
setSelectedKeyAlias,
createClicked,
}) => {
const { keys, isLoading, error, pagination, refresh, setKeys } = useKeyList({
selectedTeam: selectedTeam || undefined,
currentOrg,
selectedKeyAlias,
accessToken: accessToken || "",
createClicked,
expand: ["user"],
});
const handlePageChange = (newPage: number) => {
refresh({ page: newPage });
};
return (
<div>
<VirtualKeysTable teams={teams} organizations={organizations} />
</div>
);
};
// Update the type declaration to include the new function
declare global {
interface Window {
refreshKeysList?: () => void;
addNewKeyToList?: (newKey: any) => void;
}
}
export default ViewKeyTable;
@@ -1,62 +0,0 @@
/**
* HistoryDivider - Collapsible divider for message history
* Dashed line with expandable content
*/
import { useState } from 'react';
import { Typography } from 'antd';
import { UpOutlined, DownOutlined } from '@ant-design/icons';
import { ParsedMessage } from './prettyMessagesTypes';
import { MessageBlock } from './MessageBlock';
const { Text } = Typography;
interface HistoryDividerProps {
messages: ParsedMessage[];
}
export function HistoryDivider({ messages }: HistoryDividerProps) {
const [isExpanded, setIsExpanded] = useState(false);
if (messages.length === 0) return null;
return (
<div style={{ margin: '12px 0' }}>
{/* Dashed Divider with Label */}
<div
onClick={() => setIsExpanded(!isExpanded)}
style={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
gap: 8,
}}
>
<div style={{ flex: 1, borderTop: '1px dashed #d9d9d9' }} />
<Text type="secondary" style={{ fontSize: 11 }}>
History ({messages.length})
</Text>
{isExpanded ? (
<UpOutlined style={{ fontSize: 10, color: '#8c8c8c' }} />
) : (
<DownOutlined style={{ fontSize: 10, color: '#8c8c8c' }} />
)}
<div style={{ flex: 1, borderTop: '1px dashed #d9d9d9' }} />
</div>
{/* Expanded View - Full Messages */}
{isExpanded && (
<div style={{ marginTop: 12 }}>
{messages.map((msg, index) => (
<MessageBlock
key={index}
role={msg.role.toUpperCase()}
content={msg.content}
toolCalls={msg.toolCalls}
/>
))}
</div>
)}
</div>
);
}
@@ -1,104 +0,0 @@
/**
* MessageBlock - Displays a single message with role label
* No colors, minimal gray styling
*/
import { useState } from 'react';
import { Typography, Button } from 'antd';
import { ToolCall } from './prettyMessagesTypes';
import { ToolCallBlock } from './ToolCallBlock';
const { Text } = Typography;
interface MessageBlockProps {
role: string;
content?: string;
toolCalls?: ToolCall[];
}
const TRUNCATE_LENGTH = 500;
export function MessageBlock({ role, content, toolCalls }: MessageBlockProps) {
const [isExpanded, setIsExpanded] = useState(false);
const hasContent = content && content.length > 0;
const hasToolCalls = toolCalls && toolCalls.length > 0;
const isLong = hasContent && content.length > TRUNCATE_LENGTH;
const shouldTruncate = isLong && !isExpanded;
// If no content and no tool calls, don't render anything
if (!hasContent && !hasToolCalls) {
return null;
}
return (
<div style={{ marginBottom: 12 }}>
{/* Role Label */}
<Text
type="secondary"
style={{
fontSize: 11,
display: 'block',
marginBottom: 4,
color: '#8c8c8c',
}}
>
{role}
</Text>
{/* Content */}
{hasContent && (
<div
style={{
fontSize: 13,
lineHeight: 1.6,
color: '#262626',
marginBottom: hasToolCalls ? 8 : 0,
}}
>
{shouldTruncate ? (
<>
{content.slice(0, TRUNCATE_LENGTH)}...
<Button
type="link"
size="small"
onClick={() => setIsExpanded(true)}
style={{ padding: '0 4px', fontSize: 12 }}
>
Show more
</Button>
</>
) : (
<>
{content}
{isLong && isExpanded && (
<Button
type="link"
size="small"
onClick={() => setIsExpanded(false)}
style={{
padding: '0 4px',
fontSize: 12,
display: 'block',
marginTop: 4,
}}
>
Show less
</Button>
)}
</>
)}
</div>
)}
{/* Tool Calls */}
{hasToolCalls && (
<div>
{toolCalls.map((tool, index) => (
<ToolCallBlock key={tool.id || index} tool={tool} />
))}
</div>
)}
</div>
);
}
@@ -1,199 +0,0 @@
/**
* MessageCard - Display individual message with role-based styling
* Features: collapsible long content, copy button, tool calls display
*/
import { useState } from 'react';
import { Button, Typography, message as antdMessage } from 'antd';
import { CopyOutlined } from '@ant-design/icons';
import { ParsedMessage } from './prettyMessagesTypes';
import { ROLE_STYLES } from './prettyMessagesUtils';
import { ToolCallCard } from './ToolCallCard';
const { Text } = Typography;
interface MessageCardProps {
message: ParsedMessage;
defaultCollapsed?: boolean;
showToolCalls?: boolean;
}
const TRUNCATE_LENGTH = 500;
export function MessageCard({
message,
defaultCollapsed = false,
showToolCalls = false,
}: MessageCardProps) {
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
const [isHovered, setIsHovered] = useState(false);
const style = ROLE_STYLES[message.role] || ROLE_STYLES.user;
const content = message.content || '';
const isLong = content.length > TRUNCATE_LENGTH;
const shouldTruncate = isCollapsed && isLong;
// Don't show empty content for assistant messages with tool calls
const hasContent = content.length > 0;
const hasToolCalls = showToolCalls && message.toolCalls && message.toolCalls.length > 0;
// If assistant message with no content but has tool calls, skip null display
if (message.role === 'assistant' && !hasContent && hasToolCalls) {
return (
<div
style={{ marginBottom: 16 }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Role Label Row */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 6,
}}
>
<Text
strong
style={{
fontSize: 11,
color: style.labelColor,
letterSpacing: '0.5px',
}}
>
{style.label}
</Text>
</div>
{/* Tool Calls with left border */}
<div
style={{
borderLeft: `2px solid ${style.borderColor}`,
paddingLeft: 12,
}}
>
{message.toolCalls!.map((tool, index) => (
<ToolCallCard key={tool.id || index} tool={tool} />
))}
</div>
</div>
);
}
const handleCopy = () => {
navigator.clipboard.writeText(content);
antdMessage.success('Message copied');
};
return (
<div
style={{ marginBottom: 16 }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Role Label Row */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 6,
}}
>
<Text
strong
style={{
fontSize: 11,
color: style.labelColor,
letterSpacing: '0.5px',
}}
>
{style.label}
{isLong && (
<Text type="secondary" style={{ marginLeft: 8, fontWeight: 'normal', fontSize: 11 }}>
({content.length.toLocaleString()} chars)
</Text>
)}
</Text>
{/* Copy Button - Show on hover */}
{hasContent && (
<Button
type="text"
size="small"
icon={<CopyOutlined />}
style={{
opacity: isHovered ? 1 : 0,
transition: 'opacity 0.2s',
}}
onClick={handleCopy}
/>
)}
</div>
{/* Content with left border accent */}
{hasContent && (
<div
style={{
borderLeft: `2px solid ${style.borderColor}`,
paddingLeft: 12,
fontSize: 13,
lineHeight: 1.6,
color: '#262626',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
}}
>
{shouldTruncate ? (
<>
{content.slice(0, TRUNCATE_LENGTH)}...
<Button
type="link"
size="small"
onClick={() => setIsCollapsed(false)}
style={{ padding: '0 4px', fontSize: 12 }}
>
Show more
</Button>
</>
) : (
<>
{content}
{isLong && !isCollapsed && (
<Button
type="link"
size="small"
onClick={() => setIsCollapsed(true)}
style={{
padding: '0 4px',
display: 'block',
marginTop: 4,
fontSize: 12,
}}
>
Show less
</Button>
)}
</>
)}
</div>
)}
{/* Tool Calls (for assistant messages) */}
{hasToolCalls && hasContent && (
<div
style={{
borderLeft: `2px solid ${style.borderColor}`,
paddingLeft: 12,
marginTop: 8,
}}
>
{message.toolCalls!.map((tool, index) => (
<ToolCallCard key={tool.id || index} tool={tool} />
))}
</div>
)}
</div>
);
}
@@ -1,78 +0,0 @@
/**
* ToolCallBlock - Displays tool call with white background
* Minimal, monochrome styling
*/
import { useState } from 'react';
import { Typography, Button, message } from 'antd';
import { CopyOutlined } from '@ant-design/icons';
import { ToolCall } from './prettyMessagesTypes';
const { Text } = Typography;
interface ToolCallBlockProps {
tool: ToolCall;
}
export function ToolCallBlock({ tool }: ToolCallBlockProps) {
const [isHovered, setIsHovered] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(JSON.stringify(tool.arguments, null, 2));
message.success('Tool arguments copied');
};
return (
<div
style={{
background: '#fff',
border: '1px solid #e8e8e8',
borderRadius: 4,
padding: '8px 12px',
marginTop: 8,
fontFamily: 'monospace',
fontSize: 12,
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Tool Name Header */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: Object.keys(tool.arguments).length > 0 ? 6 : 0,
}}
>
<Text strong style={{ fontSize: 12, color: '#262626' }}>
{tool.name}
</Text>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
style={{
opacity: isHovered ? 1 : 0,
transition: 'opacity 0.2s',
}}
onClick={handleCopy}
/>
</div>
{/* Tool Arguments */}
{Object.keys(tool.arguments).length > 0 && (
<div>
{Object.entries(tool.arguments).map(([key, value]) => (
<div key={key} style={{ marginBottom: 2 }}>
<Text type="secondary" style={{ fontSize: 12 }}>
{key}:{' '}
</Text>
<Text style={{ fontSize: 12 }}>{JSON.stringify(value)}</Text>
</div>
))}
</div>
)}
</div>
);
}
@@ -1,79 +0,0 @@
/**
* ToolCallCard - Display tool call information inline in assistant messages
*/
import { useState } from 'react';
import { Button, Typography, message } from 'antd';
import { CopyOutlined } from '@ant-design/icons';
import { ToolCall } from './prettyMessagesTypes';
const { Text } = Typography;
interface ToolCallCardProps {
tool: ToolCall;
}
export function ToolCallCard({ tool }: ToolCallCardProps) {
const [isHovered, setIsHovered] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(JSON.stringify(tool.arguments, null, 2));
message.success('Tool arguments copied');
};
return (
<div
style={{
background: '#fafafa',
border: '1px solid #f0f0f0',
borderRadius: 4,
padding: '8px 12px',
marginBottom: 8,
fontFamily: 'monospace',
fontSize: 12,
}}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Tool Header */}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: Object.keys(tool.arguments).length > 0 ? 6 : 0,
}}
>
<Text strong style={{ fontSize: 12, color: '#262626' }}>
{tool.name}
</Text>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
style={{
opacity: isHovered ? 1 : 0,
transition: 'opacity 0.2s',
}}
onClick={handleCopy}
/>
</div>
{/* Tool Arguments - Simple key: value format */}
{Object.keys(tool.arguments).length > 0 && (
<div>
{Object.entries(tool.arguments).map(([key, value]) => (
<div key={key} style={{ marginBottom: 2 }}>
<Text type="secondary" style={{ fontSize: 12 }}>
{key}:
</Text>{' '}
<Text code style={{ background: 'transparent', fontSize: 12 }}>
{JSON.stringify(value)}
</Text>
</div>
))}
</div>
)}
</div>
);
}
@@ -1,22 +0,0 @@
export async function getCountryFromIP(ip: string): Promise<string> {
try {
const response = await fetch(`http://ip-api.com/json/${ip}`);
const data = await response.json();
console.log("ip lookup data", data);
// Convert country code to flag emoji (each capital letter is converted to regional indicator symbol)
const flagEmoji = data.countryCode
? data.countryCode
.toUpperCase()
.split("")
.map((char: string) => String.fromCodePoint(char.charCodeAt(0) + 127397))
.join("")
: "";
// Return country name with flag emoji
return data.country ? `${flagEmoji} ${data.country}` : "Unknown";
} catch (error) {
console.error("Error looking up IP:", error);
return "Unknown";
}
}
@@ -1,48 +0,0 @@
import { QueryClient } from "@tanstack/react-query";
import { uiSpendLogDetailsCall } from "../networking";
import { LogEntry } from "./columns";
export interface PrefetchedLog {
id: string;
messages: any;
response: any;
}
export const prefetchLogDetails = async (
logs: LogEntry[],
formattedStartTime: string,
accessToken: string,
queryClient: QueryClient,
) => {
console.log("prefetchLogDetails called with", logs.length, "logs");
const promises = logs.map((log) => {
if (log.request_id) {
console.log("Prefetching details for request_id:", log.request_id);
return queryClient.prefetchQuery({
queryKey: ["logDetails", log.request_id, formattedStartTime],
queryFn: async () => {
console.log("Fetching details for", log.request_id);
const result = (await uiSpendLogDetailsCall(
accessToken,
log.request_id,
formattedStartTime,
)) as PrefetchedLog;
console.log("Received details for", log.request_id, ":", result ? "success" : "failed");
return result;
},
staleTime: 10 * 60 * 1000, // 10 minutes
gcTime: 10 * 60 * 1000, // 10 minutes
});
}
});
try {
const results = await Promise.all(promises);
console.log("All prefetch promises completed:", results.length);
return results;
} catch (error) {
console.error("Error in prefetchLogDetails:", error);
throw error;
}
};
@@ -1,59 +0,0 @@
"use client";
import React, { useEffect, useState } from "react";
import { modelAvailableCall } from "./networking";
interface ViewUserTeamProps {
userID: string | null;
userRole: string | null;
selectedTeam: any | null;
accessToken: string | null;
}
const ViewUserTeam: React.FC<ViewUserTeamProps> = ({ userID, userRole, selectedTeam, accessToken }) => {
const [userModels, setUserModels] = useState([]);
useEffect(() => {
const fetchUserModels = async () => {
try {
if (userID === null || userRole === null) {
return;
}
if (accessToken !== null) {
const model_available = await modelAvailableCall(accessToken, userID, userRole);
let available_model_names = model_available["data"].map((element: { id: string }) => element.id);
console.log("available_model_names:", available_model_names);
setUserModels(available_model_names);
}
} catch (error) {
console.error("Error fetching user models:", error);
}
};
fetchUserModels();
}, [accessToken, userID, userRole]);
// logic to decide what models to display
let modelsToDisplay = [];
if (selectedTeam && selectedTeam.models) {
modelsToDisplay = selectedTeam.models;
}
// check if "all-proxy-models" is in modelsToDisplay
if (modelsToDisplay && modelsToDisplay.includes("all-proxy-models")) {
console.log("user models:", userModels);
modelsToDisplay = userModels;
}
return (
<>
<div className="mb-5">
<p className="text-3xl text-tremor-content-strong dark:text-dark-tremor-content-strong font-semibold">
{selectedTeam?.team_alias}
</p>
{selectedTeam?.team_id && (
<p className="text-xs text-gray-400 dark:text-gray-400 font-semibold">Team ID: {selectedTeam?.team_id}</p>
)}
</div>
</>
);
};
export default ViewUserTeam;