first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,570 @@
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { getErrorMessage, useGetCoreConfigQuery, useGetDroppedRequestsQuery, useUpdateCoreConfigMutation } from "@/lib/store";
import { CoreConfig, DefaultCoreConfig, DefaultGlobalHeaderFilterConfig, GlobalHeaderFilterConfig } from "@/lib/types/config";
import { cn } from "@/lib/utils";
import LargePayloadSettingsFragment from "@enterprise/components/large-payload/largePayloadSettingsFragment";
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
import { useGetLargePayloadConfigQuery, useUpdateLargePayloadConfigMutation } from "@enterprise/lib/store/apis/largePayloadApi";
import { DefaultLargePayloadConfig, LargePayloadConfig } from "@enterprise/lib/types/largePayload";
import { Info, Plus, X } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
// Security headers that cannot be configured in allowlist/denylist
// These headers are always blocked for security reasons regardless of configuration
const SECURITY_HEADERS = [
"proxy-authorization",
"cookie",
"host",
"content-length",
"connection",
"transfer-encoding",
"x-api-key",
"x-goog-api-key",
"x-bf-api-key",
"x-bf-vk",
];
// Helper to check if a header is a security header
function isSecurityHeader(header: string): boolean {
const h = header.toLowerCase().trim();
// Wildcard patterns are not literal security headers
if (h.includes("*")) return false;
return SECURITY_HEADERS.includes(h);
}
// Helper to compare header filter configs
function headerFilterConfigEqual(a?: GlobalHeaderFilterConfig, b?: GlobalHeaderFilterConfig): boolean {
const aAllowlist = a?.allowlist || [];
const bAllowlist = b?.allowlist || [];
const aDenylist = a?.denylist || [];
const bDenylist = b?.denylist || [];
if (aAllowlist.length !== bAllowlist.length || aDenylist.length !== bDenylist.length) {
return false;
}
return aAllowlist.every((v, i) => v === bAllowlist[i]) && aDenylist.every((v, i) => v === bDenylist[i]);
}
// Helper to compare large payload configs
function largePayloadConfigEqual(a: LargePayloadConfig, b: LargePayloadConfig): boolean {
return (
a.enabled === b.enabled &&
a.request_threshold_bytes === b.request_threshold_bytes &&
a.response_threshold_bytes === b.response_threshold_bytes &&
a.prefetch_size_bytes === b.prefetch_size_bytes &&
a.max_payload_bytes === b.max_payload_bytes &&
a.truncated_log_bytes === b.truncated_log_bytes
);
}
export default function ClientSettingsView() {
const hasSettingsUpdateAccess = useRbac(RbacResource.Settings, RbacOperation.Update);
const [droppedRequests, setDroppedRequests] = useState<number>(0);
const { data: droppedRequestsData } = useGetDroppedRequestsQuery();
const { data: bifrostConfig, isLoading: isCoreConfigLoading } = useGetCoreConfigQuery({ fromDB: true });
const config = bifrostConfig?.client_config;
const [updateCoreConfig, { isLoading: isSavingCoreConfig }] = useUpdateCoreConfigMutation();
const [localConfig, setLocalConfig] = useState<CoreConfig>(DefaultCoreConfig);
// Large payload config state
const { data: serverLargePayloadConfig, isLoading: isLargePayloadConfigLoading } = useGetLargePayloadConfigQuery();
const [updateLargePayloadConfig, { isLoading: isSavingLargePayload }] = useUpdateLargePayloadConfigMutation();
const [localLargePayloadConfig, setLocalLargePayloadConfig] = useState<LargePayloadConfig>(DefaultLargePayloadConfig);
const isQueriesLoading = isCoreConfigLoading || isLargePayloadConfigLoading;
const isLoading = isSavingCoreConfig || isSavingLargePayload;
useEffect(() => {
if (droppedRequestsData) {
setDroppedRequests(droppedRequestsData.dropped_requests);
}
}, [droppedRequestsData]);
useEffect(() => {
if (config) {
setLocalConfig({
...config,
header_filter_config: config.header_filter_config || DefaultGlobalHeaderFilterConfig,
});
}
}, [config]);
useEffect(() => {
if (serverLargePayloadConfig) {
setLocalLargePayloadConfig(serverLargePayloadConfig);
}
}, [serverLargePayloadConfig]);
const hasCoreConfigChanges = useMemo(() => {
if (!config) return false;
return (
localConfig.drop_excess_requests !== config.drop_excess_requests ||
localConfig.disable_db_pings_in_health !== config.disable_db_pings_in_health ||
localConfig.async_job_result_ttl !== config.async_job_result_ttl ||
!headerFilterConfigEqual(localConfig.header_filter_config, config.header_filter_config)
);
}, [config, localConfig]);
const hasLargePayloadChanges = useMemo(() => {
const baseline = serverLargePayloadConfig ?? DefaultLargePayloadConfig;
return !largePayloadConfigEqual(localLargePayloadConfig, baseline);
}, [serverLargePayloadConfig, localLargePayloadConfig]);
const hasChanges = hasCoreConfigChanges || hasLargePayloadChanges;
// Detect security headers in allowlist/denylist
const invalidSecurityHeaders = useMemo(() => {
const allowlist = localConfig.header_filter_config?.allowlist || [];
const denylist = localConfig.header_filter_config?.denylist || [];
const invalidInAllowlist = allowlist.filter((h) => h && isSecurityHeader(h));
const invalidInDenylist = denylist.filter((h) => h && isSecurityHeader(h));
return [...new Set([...invalidInAllowlist, ...invalidInDenylist])];
}, [localConfig.header_filter_config]);
const hasSecurityHeaderError = invalidSecurityHeaders.length > 0;
const handleConfigChange = useCallback((field: keyof CoreConfig, value: boolean | number | string[] | GlobalHeaderFilterConfig) => {
setLocalConfig((prev) => ({ ...prev, [field]: value }));
}, []);
const handleLargePayloadConfigChange = useCallback((newConfig: LargePayloadConfig) => {
setLocalLargePayloadConfig(newConfig);
}, []);
const handleSave = useCallback(async () => {
// Defense in depth - don't save if security headers are present
if (hasSecurityHeaderError) {
return;
}
// Validate large payload config if it has changes
if (hasLargePayloadChanges) {
const minBytes = 1024;
if (
localLargePayloadConfig.request_threshold_bytes < minBytes ||
localLargePayloadConfig.response_threshold_bytes < minBytes ||
localLargePayloadConfig.prefetch_size_bytes < minBytes ||
localLargePayloadConfig.max_payload_bytes < minBytes ||
localLargePayloadConfig.truncated_log_bytes < minBytes
) {
toast.error("All byte values must be at least 1024 (1 KB).");
return;
}
if (localLargePayloadConfig.max_payload_bytes < localLargePayloadConfig.request_threshold_bytes) {
toast.error("Max payload size must be greater than or equal to the request threshold.");
return;
}
if (localLargePayloadConfig.max_payload_bytes < localLargePayloadConfig.response_threshold_bytes) {
toast.error("Max payload size must be greater than or equal to the response threshold.");
return;
}
}
let coreConfigSaved = false;
let largePayloadSaved = false;
// Save core config if changed
if (hasCoreConfigChanges) {
if (!bifrostConfig) {
toast.error("Configuration not loaded. Please refresh and try again.");
return;
}
// Clean up empty strings from header filter config
const cleanedConfig = {
...localConfig,
header_filter_config: {
allowlist: (localConfig.header_filter_config?.allowlist || []).filter((h) => h && h.trim().length > 0),
denylist: (localConfig.header_filter_config?.denylist || []).filter((h) => h && h.trim().length > 0),
},
};
try {
await updateCoreConfig({ ...bifrostConfig!, client_config: cleanedConfig }).unwrap();
coreConfigSaved = true;
} catch (error) {
toast.error(`Failed to save client config: ${getErrorMessage(error)}`);
}
}
// Save large payload config if changed
if (hasLargePayloadChanges) {
try {
await updateLargePayloadConfig(localLargePayloadConfig).unwrap();
largePayloadSaved = true;
} catch (error) {
toast.error(`Failed to save large payload config: ${getErrorMessage(error)}`);
}
}
if (coreConfigSaved || largePayloadSaved) {
if (largePayloadSaved) {
toast.success("Settings updated. Large payload changes require a restart to apply.");
} else {
toast.success("Client settings updated successfully.");
}
}
}, [
bifrostConfig,
hasSecurityHeaderError,
hasCoreConfigChanges,
hasLargePayloadChanges,
localConfig,
localLargePayloadConfig,
updateCoreConfig,
updateLargePayloadConfig,
]);
// Header filter list handlers
const handleAddAllowlistHeader = useCallback(() => {
setLocalConfig((prev) => ({
...prev,
header_filter_config: {
...prev.header_filter_config,
allowlist: [...(prev.header_filter_config?.allowlist || []), ""],
},
}));
}, []);
const handleRemoveAllowlistHeader = useCallback((index: number) => {
setLocalConfig((prev) => ({
...prev,
header_filter_config: {
...prev.header_filter_config,
allowlist: (prev.header_filter_config?.allowlist || []).filter((_, i) => i !== index),
},
}));
}, []);
const handleAllowlistChange = useCallback((index: number, value: string) => {
const lowerValue = value.toLowerCase();
setLocalConfig((prev) => ({
...prev,
header_filter_config: {
...prev.header_filter_config,
allowlist: (prev.header_filter_config?.allowlist || []).map((h, i) => (i === index ? lowerValue : h)),
},
}));
}, []);
const handleAddDenylistHeader = useCallback(() => {
setLocalConfig((prev) => ({
...prev,
header_filter_config: {
...prev.header_filter_config,
denylist: [...(prev.header_filter_config?.denylist || []), ""],
},
}));
}, []);
const handleRemoveDenylistHeader = useCallback((index: number) => {
setLocalConfig((prev) => ({
...prev,
header_filter_config: {
...prev.header_filter_config,
denylist: (prev.header_filter_config?.denylist || []).filter((_, i) => i !== index),
},
}));
}, []);
const handleDenylistChange = useCallback((index: number, value: string) => {
const lowerValue = value.toLowerCase();
setLocalConfig((prev) => ({
...prev,
header_filter_config: {
...prev.header_filter_config,
denylist: (prev.header_filter_config?.denylist || []).map((h, i) => (i === index ? lowerValue : h)),
},
}));
}, []);
return (
<div className="mx-auto w-full max-w-4xl space-y-6">
<div>
<h2 className="text-lg font-semibold tracking-tight">Client Settings</h2>
<p className="text-muted-foreground text-sm">Configure client behavior and request handling.</p>
</div>
<div className="space-y-4">
{/* Drop Excess Requests */}
<div className="flex items-center justify-between space-x-2">
<div className="space-y-0.5">
<label htmlFor="drop-excess-requests" className="text-sm font-medium">
Drop Excess Requests
</label>
<p className="text-muted-foreground text-sm">
If enabled, Bifrost will drop requests that exceed pool capacity.{" "}
{localConfig.drop_excess_requests && droppedRequests > 0 ? (
<span>
Have dropped <b>{droppedRequests} requests</b> since last restart.
</span>
) : (
<></>
)}
</p>
</div>
<Switch
id="drop-excess-requests"
size="md"
checked={localConfig.drop_excess_requests}
onCheckedChange={(checked) => handleConfigChange("drop_excess_requests", checked)}
disabled={!hasSettingsUpdateAccess}
/>
</div>
{/* Disable DB Pings in Health */}
<div className="flex items-center justify-between space-x-2">
<div className="space-y-0.5">
<label htmlFor="disable-db-pings-in-health" className="text-sm font-medium">
Disable DB Pings in Health Check
</label>
<p className="text-muted-foreground text-sm">
If enabled, the /health endpoint will skip database connectivity checks and return OK immediately.
</p>
</div>
<Switch
id="disable-db-pings-in-health"
size="md"
checked={localConfig.disable_db_pings_in_health}
onCheckedChange={(checked) => handleConfigChange("disable_db_pings_in_health", checked)}
disabled={!hasSettingsUpdateAccess}
/>
</div>
{/* Async Job Result TTL */}
<div className="flex items-center justify-between space-x-2">
<div className="space-y-0.5">
<label htmlFor="async-job-result-ttl" className="text-sm font-medium">
Async Job Result TTL (seconds)
</label>
<p className="text-muted-foreground text-sm">
Default time-to-live for async job results in seconds. Results are automatically cleaned up after expiry.
</p>
</div>
<Input
id="async-job-result-ttl"
type="number"
min={1}
className="w-32"
value={localConfig.async_job_result_ttl}
onChange={(e) => handleConfigChange("async_job_result_ttl", parseInt(e.target.value) || 0)}
disabled={!hasSettingsUpdateAccess}
data-testid="client-settings-async-job-result-ttl-input"
/>
</div>
</div>
{/* Header Filter Section */}
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold tracking-tight">Header Forwarding</h3>
<p className="text-muted-foreground text-sm">Control which extra headers are forwarded to LLM providers.</p>
</div>
<Accordion type="multiple" className="w-full rounded-sm border px-4">
<AccordionItem value="about-extra-headers">
<AccordionTrigger>
<span className="flex items-center gap-2">
<Info className="h-4 w-4" />
About Header Forwarding
</span>
</AccordionTrigger>
<AccordionContent className="space-y-3">
<div>
<p className="mb-2 font-medium">Two ways to forward headers:</p>
<ul className="text-muted-foreground list-inside list-disc space-y-1 text-sm">
<li>
<span className="font-medium">Prefixed headers:</span> Use{" "}
<code className="bg-muted rounded px-1 py-0.5 font-mono text-xs">x-bf-eh-*</code> prefix. For example,{" "}
<code className="bg-muted rounded px-1 py-0.5 font-mono text-xs">x-bf-eh-custom-id</code> is forwarded as{" "}
<code className="bg-muted rounded px-1 py-0.5 font-mono text-xs">custom-id</code>.
</li>
<li>
<span className="font-medium">Direct headers:</span> Any header explicitly added to the allowlist can be forwarded
directly without the prefix (e.g.,{" "}
<code className="bg-muted rounded px-1 py-0.5 font-mono text-xs">anthropic-beta</code>).
</li>
</ul>
</div>
<div>
<p className="mb-2 font-medium">How allowlist and denylist work:</p>
<ul className="text-muted-foreground list-inside list-disc space-y-1 text-sm">
<li>
<span className="font-medium">Allowlist empty:</span> Only{" "}
<code className="bg-muted rounded px-1 py-0.5 font-mono text-xs">x-bf-eh-*</code> prefixed headers are forwarded
(default behavior)
</li>
<li>
<span className="font-medium">Allowlist configured:</span> Prefixed headers filtered by allowlist, plus any direct
header in the allowlist is forwarded
</li>
<li>
<span className="font-medium">Denylist:</span> Headers in the denylist are always blocked from forwarding
</li>
<li>
<span className="font-medium">Wildcards:</span> Use{" "}
<code className="bg-muted rounded px-1 py-0.5 font-mono text-xs">*</code> at the end of a pattern to match prefixes
(e.g., <code className="bg-muted rounded px-1 py-0.5 font-mono text-xs">anthropic-*</code> matches all headers starting
with <code className="bg-muted rounded px-1 py-0.5 font-mono text-xs">anthropic-</code>). Use{" "}
<code className="bg-muted rounded px-1 py-0.5 font-mono text-xs">*</code> alone to match all headers.
</li>
</ul>
</div>
<div>
<p className="mb-2 font-medium">Important:</p>
<ul className="text-muted-foreground list-inside list-disc space-y-1 text-sm">
<li>
Allowlist/denylist entries should be the header name <span className="font-medium">without</span> the{" "}
<code className="bg-muted rounded px-1 py-0.5 font-mono text-xs">x-bf-eh-</code> prefix
</li>
<li>
Example: To allow <code className="bg-muted rounded px-1 py-0.5 font-mono text-xs">x-bf-eh-custom-id</code> or direct{" "}
<code className="bg-muted rounded px-1 py-0.5 font-mono text-xs">custom-id</code>, add{" "}
<code className="bg-muted rounded px-1 py-0.5 font-mono text-xs">custom-id</code> to the allowlist
</li>
</ul>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="security-note">
<AccordionTrigger>
<span className="flex items-center gap-2">
<Info className="h-4 w-4" />
Security Note
</span>
</AccordionTrigger>
<AccordionContent>
<p className="text-sm">
Some headers are always blocked for security reasons regardless of configuration. These headers cannot be added to the
allowlist or denylist:
</p>
<p className="text-muted-foreground mt-1 font-mono text-xs">
proxy-authorization, cookie, host, content-length, connection, transfer-encoding, x-api-key, x-goog-api-key, x-bf-api-key,
x-bf-vk
</p>
</AccordionContent>
</AccordionItem>
</Accordion>
{/* Allowlist Section */}
<div className="space-y-3">
<div className="space-y-1">
<h4 className="text-sm font-medium">Allowlist</h4>
<p className="text-muted-foreground text-xs">
Headers to allow. Enter names without the <code className="bg-muted rounded px-1 font-mono">x-bf-eh-</code> prefix. Any header
in this list can also be sent directly without the prefix.
</p>
</div>
<div className="space-y-2">
{(localConfig.header_filter_config?.allowlist || []).map((header, index) => (
<div key={index} className="flex items-center gap-2">
<Input
placeholder="e.g. anthropic-*, custom-id"
data-testid="header-filter-allowlist-input"
className={cn(
"font-mono lowercase",
isSecurityHeader(header) &&
"border-destructive focus:border-destructive focus-visible:border-destructive focus-visible:ring-destructive/50",
)}
value={header}
onChange={(e) => handleAllowlistChange(index, e.target.value)}
disabled={!hasSettingsUpdateAccess}
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => handleRemoveAllowlistHeader(index)}
className="text-muted-foreground hover:text-destructive"
disabled={!hasSettingsUpdateAccess}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
<Button type="button" variant="outline" size="sm" onClick={handleAddAllowlistHeader} disabled={!hasSettingsUpdateAccess}>
<Plus className="mr-2 h-4 w-4" />
Add Header
</Button>
</div>
</div>
{/* Denylist Section */}
<div className="space-y-3">
<div className="space-y-1">
<h4 className="text-sm font-medium">Denylist</h4>
<p className="text-muted-foreground text-xs">
Headers to block. Enter names without the <code className="bg-muted rounded px-1 font-mono">x-bf-eh-</code> prefix. Applies to
both prefixed and direct header forwarding.
</p>
</div>
<div className="space-y-2">
{(localConfig.header_filter_config?.denylist || []).map((header, index) => (
<div key={index} className="flex items-center gap-2">
<Input
placeholder="e.g. x-internal-*"
data-testid="header-filter-denylist-input"
className={cn(
"font-mono lowercase",
isSecurityHeader(header) &&
"border-destructive focus:border-destructive focus-visible:border-destructive focus-visible:ring-destructive/50",
)}
value={header}
onChange={(e) => handleDenylistChange(index, e.target.value)}
disabled={!hasSettingsUpdateAccess}
/>
<Button
type="button"
variant="ghost"
size="icon"
onClick={() => handleRemoveDenylistHeader(index)}
className="text-muted-foreground hover:text-destructive"
disabled={!hasSettingsUpdateAccess}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
<Button type="button" variant="outline" size="sm" onClick={handleAddDenylistHeader} disabled={!hasSettingsUpdateAccess}>
<Plus className="mr-2 h-4 w-4" />
Add Header
</Button>
</div>
</div>
</div>
{/* Large Payload Optimization - Enterprise only */}
<LargePayloadSettingsFragment
config={localLargePayloadConfig}
onConfigChange={handleLargePayloadConfigChange}
controlsDisabled={isLoading || !hasSettingsUpdateAccess}
/>
<div className="flex justify-end pt-2">
{hasSecurityHeaderError ? (
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button disabled>{isLoading ? "Saving..." : "Save Changes"}</Button>
</span>
</TooltipTrigger>
<TooltipContent>
Remove security header{invalidSecurityHeaders.length > 1 ? "s" : ""}: {invalidSecurityHeaders.join(", ")}
</TooltipContent>
</Tooltip>
) : (
<Button onClick={handleSave} disabled={!hasChanges || isLoading || isQueriesLoading || !hasSettingsUpdateAccess}>
{isLoading ? "Saving..." : "Save Changes"}
</Button>
)}
</div>
</div>
);
}