mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-10 11:03:42 +00:00
[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:
@@ -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;
|
||||
-203
@@ -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;
|
||||
-71
@@ -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;
|
||||
|
||||
-276
@@ -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);
|
||||
};
|
||||
|
||||
-104
@@ -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;
|
||||
|
||||
-87
@@ -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;
|
||||
-243
@@ -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)
|
||||
`,
|
||||
};
|
||||
-184
@@ -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;
|
||||
Reference in New Issue
Block a user