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,195 @@
import { FormControl, FormField, FormItem, FormLabel } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { BaseProvider, RequestType } from "@/lib/types/config";
import { isRequestTypeDisabled } from "@/lib/utils/validation";
import { Settings2 } from "lucide-react";
import { useEffect, useMemo } from "react";
import { Control, useFormContext } from "react-hook-form";
interface AllowedRequestsFieldsProps {
control: Control<any>;
namePrefix?: string;
pathOverridesPrefix?: string;
providerType?: BaseProvider;
disabled?: boolean;
}
// Provider-specific endpoint paths
const ProviderEndpoints: Partial<Record<BaseProvider, Partial<Record<RequestType, string>>>> = {
openai: {
list_models: "/v1/models",
text_completion: "/v1/completions",
text_completion_stream: "/v1/completions",
chat_completion: "/v1/chat/completions",
chat_completion_stream: "/v1/chat/completions",
responses: "/v1/responses",
responses_stream: "/v1/responses",
embedding: "/v1/embeddings",
speech: "/v1/audio/speech",
speech_stream: "/v1/audio/speech",
transcription: "/v1/audio/transcriptions",
transcription_stream: "/v1/audio/transcriptions",
image_generation: "/v1/images/generations",
image_generation_stream: "/v1/images/generations",
image_edit: "/v1/images/edits",
image_edit_stream: "/v1/images/edits",
image_variation: "/v1/images/variations",
count_tokens: "/v1/responses/tokens",
},
anthropic: {
chat_completion: "/v1/messages",
chat_completion_stream: "/v1/messages",
responses: "/v1/messages",
responses_stream: "/v1/messages",
},
cohere: {
chat_completion: "/v2/chat",
chat_completion_stream: "/v2/chat",
responses: "/v2/chat",
responses_stream: "/v2/chat",
embedding: "/v2/embed",
},
};
// Helper function to get the appropriate placeholder
const getPlaceholder = (providerType: BaseProvider | undefined, requestKey: RequestType): string => {
if (providerType && ProviderEndpoints[providerType]?.[requestKey]) {
return ProviderEndpoints[providerType][requestKey]!;
}
return ProviderEndpoints["openai"]?.[requestKey] ?? "";
};
const RequestTypes: Array<{ key: RequestType; label: string }> = [
{ key: "list_models", label: "List Models" },
{ key: "text_completion", label: "Text Completion" },
{ key: "text_completion_stream", label: "Text Completion Stream" },
{ key: "chat_completion", label: "Chat Completion" },
{ key: "chat_completion_stream", label: "Chat Completion Stream" },
{ key: "responses", label: "Responses" },
{ key: "responses_stream", label: "Responses Stream" },
{ key: "embedding", label: "Embedding" },
{ key: "speech", label: "Speech" },
{ key: "speech_stream", label: "Speech Stream" },
{ key: "transcription", label: "Transcription" },
{ key: "transcription_stream", label: "Transcription Stream" },
{ key: "image_generation", label: "Image Generation" },
{ key: "image_generation_stream", label: "Image Generation Stream" },
{ key: "image_edit", label: "Image Edit" },
{ key: "image_edit_stream", label: "Image Edit Stream" },
{ key: "image_variation", label: "Image Variation" },
{ key: "count_tokens", label: "Count Tokens" },
];
export function AllowedRequestsFields({
control,
namePrefix = "allowed_requests",
pathOverridesPrefix = "request_path_overrides",
providerType,
disabled = false,
}: AllowedRequestsFieldsProps) {
const leftColumn = RequestTypes.slice(0, RequestTypes.length / 2);
const rightColumn = RequestTypes.slice(RequestTypes.length / 2);
const { getValues, setValue } = useFormContext();
// Reset disabled fields when providerType changes
useEffect(() => {
RequestTypes.forEach(({ key }) => {
const fieldName = `${namePrefix}.${key}`;
setValue(fieldName, !isRequestTypeDisabled(providerType, key), { shouldDirty: true });
});
}, [providerType, namePrefix, setValue, getValues]);
const isPathOverrideDisabled = useMemo(() => providerType === "gemini" || providerType === "bedrock", [providerType]);
const renderRequestField = (requestType: { key: RequestType; label: string }) => {
const isDisabled = isRequestTypeDisabled(providerType, requestType.key);
const placeholder = getPlaceholder(providerType, requestType.key);
return (
<FormField
key={requestType.key}
control={control}
name={`${namePrefix}.${requestType.key}`}
render={({ field: allowedField }) => (
<FormItem
className={`flex flex-row items-center justify-between rounded-lg border p-3 ${isDisabled ? "bg-muted/30 opacity-60" : ""}`}
>
<div className="space-y-0.5">
<FormLabel className={isDisabled ? "cursor-not-allowed" : ""}>{requestType.label}</FormLabel>
</div>
<div className="flex items-center gap-2">
{/* Settings icon for path override - only show when enabled */}
{allowedField.value && !isDisabled && !isPathOverrideDisabled && !disabled && (
<FormField
control={control}
name={`${pathOverridesPrefix}.${requestType.key}`}
render={({ field: pathField }) => (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="Customize endpoint path"
>
<Settings2 className="h-4 w-4" />
</button>
</PopoverTrigger>
<PopoverContent className="w-80" align="end" onOpenAutoFocus={(e) => e.preventDefault()}>
<div className="space-y-2">
<h4 className="text-sm font-medium">Custom Path or URL</h4>
<p className="text-muted-foreground text-xs">
Override with a path (e.g., /v1/chat) or a full URL (e.g., https://api.example.com/chat) to bypass base_url
</p>
<Input placeholder={placeholder} {...pathField} value={pathField.value || ""} className="h-9" />
</div>
</PopoverContent>
</Popover>
)}
/>
)}
<FormControl>
{isDisabled ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<Switch checked={isDisabled ? false : allowedField.value} disabled={true} size="md" />
</div>
</TooltipTrigger>
<TooltipContent>
<p>Not supported by {providerType}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<Switch checked={allowedField.value} onCheckedChange={allowedField.onChange} size="md" disabled={disabled} />
)}
</FormControl>
</div>
</FormItem>
)}
/>
);
};
return (
<div className="space-y-4">
<div>
<div className="text-sm font-medium">Allowed Request Types</div>
<p className="text-muted-foreground text-xs">
Select which request types this custom provider can handle.{" "}
{!isPathOverrideDisabled ? "Click the settings icon to customize endpoint paths or use full URLs." : ""}
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-3">{leftColumn.map(renderRequestField)}</div>
<div className="space-y-3">{rightColumn.map(renderRequestField)}</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,967 @@
import { EnvVarInput } from "@/components/ui/envVarInput";
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { HeadersTable, type CellRenderParams } from "@/components/ui/headersTable";
import { Input } from "@/components/ui/input";
import { ModelMultiselect } from "@/components/ui/modelMultiselect";
import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { TagInput } from "@/components/ui/tagInput";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { isRedacted } from "@/lib/utils/validation";
import { Info } from "lucide-react";
import { useEffect, useState } from "react";
import { Control, UseFormReturn } from "react-hook-form";
// Providers that support batch APIs
const BATCH_SUPPORTED_PROVIDERS = ["openai", "bedrock", "anthropic", "gemini", "azure"];
/** Normalize form value (object or legacy JSON string) for the alias map editor. */
function normalizeAliasesValue(v: Record<string, string> | string | undefined | null): Record<string, string> {
if (v == null) {
return {};
}
if (typeof v === "string") {
const t = v.trim();
if (!t) {
return {};
}
try {
const p = JSON.parse(t) as unknown;
if (typeof p === "object" && p !== null && !Array.isArray(p)) {
return Object.fromEntries(Object.entries(p as Record<string, unknown>).map(([k, val]) => [k, String(val ?? "")]));
}
} catch {
return {};
}
return {};
}
if (typeof v === "object" && !Array.isArray(v)) {
return Object.fromEntries(Object.entries(v).map(([k, val]) => [k, typeof val === "string" ? val : String(val ?? "")]));
}
return {};
}
interface Props {
control: Control<any>;
providerName: string;
form: UseFormReturn<any>;
}
// Batch API form field for all providers
function BatchAPIFormField({ control }: { control: Control<any>; form: UseFormReturn<any> }) {
return (
<FormField
control={control}
name={`key.use_for_batch_api`}
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-sm border p-2">
<div className="space-y-1.5">
<FormLabel>Use for Batch APIs</FormLabel>
<FormDescription>
Enable this key for batch API operations. Only keys with this enabled will be used for batch requests.
</FormDescription>
</div>
<FormControl>
<Switch checked={field.value ?? false} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
);
}
export function ApiKeyFormFragment({ control, providerName, form }: Props) {
const isBedrock = providerName === "bedrock";
const isVertex = providerName === "vertex";
const isAzure = providerName === "azure";
const isReplicate = providerName === "replicate";
const isVLLM = providerName === "vllm";
const isOllama = providerName === "ollama";
const isSGL = providerName === "sgl";
const isKeylessProvider = isOllama || isSGL;
const supportsBatchAPI = BATCH_SUPPORTED_PROVIDERS.includes(providerName);
// Auth type state for Azure: 'api_key', 'entra_id', or 'default_credential'
const [azureAuthType, setAzureAuthType] = useState<"api_key" | "entra_id" | "default_credential">("api_key");
// Auth type state for Bedrock: 'iam_role', 'explicit', or 'api_key'
const [bedrockAuthType, setBedrockAuthType] = useState<"iam_role" | "explicit" | "api_key">("iam_role");
// Auth type state for Vertex: 'service_account', 'service_account_json', or 'api_key'
const [vertexAuthType, setVertexAuthType] = useState<"service_account" | "service_account_json" | "api_key">("service_account");
// Detect auth type from existing form values when editing
useEffect(() => {
if (form.formState.isDirty) return;
if (isAzure) {
const clientId = form.getValues("key.azure_key_config.client_id");
const clientSecret = form.getValues("key.azure_key_config.client_secret");
const tenantId = form.getValues("key.azure_key_config.tenant_id");
const apiKey = form.getValues("key.value");
const hasEntraField =
clientId?.value || clientId?.env_var || clientSecret?.value || clientSecret?.env_var || tenantId?.value || tenantId?.env_var;
const hasApiKey = apiKey?.value || apiKey?.env_var;
let detected: "api_key" | "entra_id" | "default_credential" = "api_key";
if (hasEntraField) {
detected = "entra_id";
} else if (!hasApiKey) {
detected = "default_credential";
}
setAzureAuthType(detected);
form.setValue("key.azure_key_config._auth_type", detected);
}
}, [isAzure, form]);
useEffect(() => {
if (form.formState.isDirty) return;
if (isVertex) {
const authCredentials = form.getValues("key.vertex_key_config.auth_credentials")?.value;
const authCredentialsEnv = form.getValues("key.vertex_key_config.auth_credentials")?.env_var;
const apiKey = form.getValues("key.value")?.value;
const apiKeyEnv = form.getValues("key.value")?.env_var;
let detected: "service_account" | "service_account_json" | "api_key" = "service_account";
if (authCredentials || authCredentialsEnv) {
detected = "service_account_json";
} else if (apiKey || apiKeyEnv) {
detected = "api_key";
}
setVertexAuthType(detected);
form.setValue("key.vertex_key_config._auth_type", detected);
}
}, [isVertex, form]);
useEffect(() => {
if (form.formState.isDirty) return;
if (isBedrock) {
const accessKey = form.getValues("key.bedrock_key_config.access_key");
const secretKey = form.getValues("key.bedrock_key_config.secret_key");
const apiKey = form.getValues("key.value");
const hasExplicitCreds = accessKey?.value || accessKey?.env_var || secretKey?.value || secretKey?.env_var;
const hasApiKey = apiKey?.value || apiKey?.env_var;
let detected: "iam_role" | "explicit" | "api_key" = "iam_role";
if (hasExplicitCreds) {
detected = "explicit";
} else if (hasApiKey) {
detected = "api_key";
}
setBedrockAuthType(detected);
form.setValue("key.bedrock_key_config._auth_type", detected);
}
}, [isBedrock, form]);
return (
<div data-tab="api-keys" className="space-y-4 overflow-hidden">
<div className="flex items-start gap-4">
<div className="flex-1">
<FormField
control={control}
name={`key.name`}
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Production Key" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={control}
name={`key.weight`}
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>Weight</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Info className="text-muted-foreground h-3 w-3" />
</span>
</TooltipTrigger>
<TooltipContent>
<p>Determines traffic distribution between keys. Higher weights receive more requests.</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<Input
placeholder="1.0"
className="w-[260px]"
value={field.value === undefined || field.value === null ? "" : String(field.value)}
onChange={(e) => {
// Keep as string during typing to allow partial input
field.onChange(e.target.value === "" ? "" : e.target.value);
}}
onBlur={(e) => {
const v = e.target.value.trim();
if (v !== "") {
const num = parseFloat(v);
if (!isNaN(num)) {
field.onChange(num);
}
}
field.onBlur();
}}
name={field.name}
ref={field.ref}
type="text"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Hide API Key field for providers with dedicated auth tabs */}
{!isAzure && !isBedrock && !isVertex && (
<FormField
control={control}
name={`key.value`}
render={({ field }) => (
<FormItem>
<FormLabel>API Key {isVLLM ? "(Optional)" : ""}</FormLabel>
<FormControl>
<EnvVarInput placeholder="API Key or env.MY_KEY" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{!isVLLM && (
<>
<FormField
control={control}
name={`key.models`}
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>Allowed Models</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Info className="text-muted-foreground h-3 w-3" />
</span>
</TooltipTrigger>
<TooltipContent>
<p>
Select specific models this key applies to, or choose "Allow All Models" to allow all. Leave empty to deny all.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<ModelMultiselect
data-testid="api-keys-models-multiselect"
provider={providerName}
allowAllOption={true}
value={field.value || []}
onChange={(models: string[]) => {
const hadStar = (field.value || []).includes("*");
const hasStar = models.includes("*");
if (!hadStar && hasStar) {
field.onChange(["*"]);
} else if (hadStar && hasStar && models.length > 1) {
field.onChange(models.filter((m: string) => m !== "*"));
} else {
field.onChange(models);
}
}}
placeholder={
(field.value || []).includes("*")
? "All models allowed"
: (field.value || []).length === 0
? "No models (deny all)"
: "Search models..."
}
unfiltered={true}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={`key.blacklisted_models`}
render={({ field }) => (
<FormItem data-testid="apikey-blacklisted-models-field">
<div className="flex items-center gap-2">
<FormLabel>Blocked Models</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Info className="text-muted-foreground h-3 w-3" />
</span>
</TooltipTrigger>
<TooltipContent className="max-w-sm">
<p>
Models this key must never serve. The denylist always wins if a model appears in both Allowed Models and here,
it is blocked. Select "All Models" to block every model on this key.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<ModelMultiselect
data-testid="api-keys-blocked-models-multiselect"
provider={providerName}
allowAllOption={true}
value={field.value || []}
onChange={(models: string[]) => {
const hadStar = (field.value || []).includes("*");
const hasStar = models.includes("*");
if (!hadStar && hasStar) {
field.onChange(["*"]);
} else if (hadStar && hasStar && models.length > 1) {
field.onChange(models.filter((m: string) => m !== "*"));
} else {
field.onChange(models);
}
}}
placeholder={
(field.value || []).includes("*")
? "All models blocked"
: (field.value || []).length === 0
? "No models blocked"
: "Search models..."
}
unfiltered={true}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={`key.aliases`}
render={({ field }) => (
<FormItem data-testid="apikey-aliases-field">
<FormLabel>Aliases (Optional)</FormLabel>
<FormDescription>
Map each request model name to the provider&apos;s identifier (deployment name, inference profile ID, fine-tuned endpoint
ID, etc.) or just a custom name, e.g. &quot;claude-sonnet-4-5&quot; -&gt; &quot;custom-claude-4.5-sonnet&quot;.
</FormDescription>
<FormControl>
<div data-testid="apikey-aliases-table">
<HeadersTable
label=""
value={normalizeAliasesValue(field.value)}
onChange={(next) => {
form.clearErrors("key.aliases");
field.onChange(Object.keys(next).length > 0 ? next : {});
}}
keyPlaceholder="Request model name"
valuePlaceholder="Deployment / profile / resource ID"
renderValueInput={({ value: cellValue, onChange, placeholder, disabled }: CellRenderParams) => (
<ModelMultiselect
isSingleSelect
provider={providerName}
value={cellValue}
onChange={onChange}
placeholder={placeholder ?? "Deployment / profile / resource ID"}
disabled={disabled}
unfiltered={true}
/>
)}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{supportsBatchAPI && !isBedrock && !isAzure && <BatchAPIFormField control={control} form={form} />}
{isAzure && (
<div className="space-y-4">
<Separator className="my-6" />
<div className="space-y-2">
<FormLabel>Authentication Method</FormLabel>
<Tabs
value={azureAuthType}
onValueChange={(v) => {
setAzureAuthType(v as "api_key" | "entra_id" | "default_credential");
form.setValue("key.azure_key_config._auth_type", v, { shouldDirty: true, shouldValidate: true });
if (v === "entra_id" || v === "default_credential") {
// Clear API key when switching away from API Key
form.setValue("key.value", undefined, { shouldDirty: true });
}
if (v === "api_key" || v === "default_credential") {
// Clear Entra ID fields when switching away from Entra ID
form.setValue("key.azure_key_config.client_id", undefined, { shouldDirty: true });
form.setValue("key.azure_key_config.client_secret", undefined, { shouldDirty: true });
form.setValue("key.azure_key_config.tenant_id", undefined, { shouldDirty: true });
form.setValue("key.azure_key_config.scopes", undefined, { shouldDirty: true });
}
}}
>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger data-testid="apikey-azure-default-credential-tab" value="default_credential">
Default Credential
</TabsTrigger>
<TabsTrigger data-testid="apikey-azure-api-key-tab" value="api_key">
API Key
</TabsTrigger>
<TabsTrigger data-testid="apikey-azure-entra-id-tab" value="entra_id">
Entra ID (Service Principal)
</TabsTrigger>
</TabsList>
</Tabs>
</div>
{azureAuthType === "api_key" && (
<FormField
control={control}
name={`key.value`}
render={({ field }) => (
<FormItem>
<FormLabel>
API Key {isVertex ? "(Supported only for gemini and fine-tuned models)" : isVLLM ? "(Optional)" : ""}
</FormLabel>
<FormControl>
<EnvVarInput placeholder="API Key or env.MY_KEY" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{azureAuthType === "default_credential" && (
<p className="text-muted-foreground text-sm">
Uses DefaultAzureCredential automatically detects managed identity on Azure VMs and containers, workload identity in AKS,
environment variables, and Azure CLI. No credentials required.
</p>
)}
<FormField
control={control}
name={`key.azure_key_config.endpoint`}
render={({ field }) => (
<FormItem>
<FormLabel>Endpoint (Required)</FormLabel>
<FormControl>
<EnvVarInput placeholder="https://your-resource.openai.azure.com or env.AZURE_ENDPOINT" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={`key.azure_key_config.api_version`}
render={({ field }) => (
<FormItem>
<FormLabel>API Version (Optional)</FormLabel>
<FormControl>
<EnvVarInput placeholder="2024-02-01 or env.AZURE_API_VERSION" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{azureAuthType === "entra_id" && (
<>
<FormField
control={control}
name={`key.azure_key_config.client_id`}
render={({ field }) => (
<FormItem>
<FormLabel>Client ID (Required)</FormLabel>
<FormControl>
<EnvVarInput placeholder="your-client-id or env.AZURE_CLIENT_ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={`key.azure_key_config.client_secret`}
render={({ field }) => (
<FormItem>
<FormLabel>Client Secret (Required)</FormLabel>
<FormControl>
<EnvVarInput placeholder="your-client-secret or env.AZURE_CLIENT_SECRET" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={`key.azure_key_config.tenant_id`}
render={({ field }) => (
<FormItem>
<FormLabel>Tenant ID (Required)</FormLabel>
<FormControl>
<EnvVarInput placeholder="your-tenant-id or env.AZURE_TENANT_ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={`key.azure_key_config.scopes`}
render={({ field }) => (
<FormItem>
<div className="flex items-center gap-2">
<FormLabel>Scopes (Optional)</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Info className="text-muted-foreground h-3 w-3" />
</span>
</TooltipTrigger>
<TooltipContent>
<p>
Optional OAuth scopes for token requests. By default we use https://cognitiveservices.azure.com/.default - add
additional scopes here if your setup requires extra permissions.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormControl>
<TagInput
data-testid="apikey-azure-scopes-input"
placeholder="Add scope (Enter or comma)"
value={field.value ?? []}
onValueChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{supportsBatchAPI && <BatchAPIFormField control={control} form={form} />}
</div>
)}
{isVertex && (
<div className="space-y-4">
<Separator className="my-6" />
<div className="space-y-2">
<FormLabel>Authentication Method</FormLabel>
<Tabs
value={vertexAuthType}
onValueChange={(v) => {
setVertexAuthType(v as "service_account" | "service_account_json" | "api_key");
form.setValue("key.vertex_key_config._auth_type", v, { shouldDirty: true, shouldValidate: true });
if (v === "service_account" || v === "api_key") {
// Clear auth credentials when switching away from service account JSON
form.setValue("key.vertex_key_config.auth_credentials", undefined, { shouldDirty: true });
}
if (v === "service_account" || v === "service_account_json") {
// Clear API key when switching away from API Key
form.setValue("key.value", undefined, { shouldDirty: true });
}
}}
>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger data-testid="apikey-vertex-service-account-tab" value="service_account">
Service Account (Attached)
</TabsTrigger>
<TabsTrigger data-testid="apikey-vertex-service-account-json-tab" value="service_account_json">
Service Account (JSON)
</TabsTrigger>
<TabsTrigger data-testid="apikey-vertex-api-key-tab" value="api_key">
API Key
</TabsTrigger>
</TabsList>
</Tabs>
{vertexAuthType === "service_account" && (
<p className="text-muted-foreground text-sm">
Uses the service account attached to your environment (GCE, GKE, Cloud Run). No credentials required.
</p>
)}
</div>
<FormField
control={control}
name={`key.vertex_key_config.project_id`}
render={({ field }) => (
<FormItem>
<FormLabel>Project ID (Required)</FormLabel>
<FormControl>
<EnvVarInput placeholder="your-gcp-project-id or env.VERTEX_PROJECT_ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={`key.vertex_key_config.project_number`}
render={({ field }) => (
<FormItem>
<FormLabel>Project Number (Required only for fine-tuned models)</FormLabel>
<FormControl>
<EnvVarInput placeholder="your-gcp-project-number or env.VERTEX_PROJECT_NUMBER" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={`key.vertex_key_config.region`}
render={({ field }) => (
<FormItem>
<FormLabel>Region (Required)</FormLabel>
<FormControl>
<EnvVarInput placeholder="us-central1 or env.VERTEX_REGION" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{vertexAuthType === "service_account_json" && (
<FormField
control={control}
name={`key.vertex_key_config.auth_credentials`}
render={({ field }) => (
<FormItem>
<FormLabel>Auth Credentials (Required)</FormLabel>
<FormDescription>Service account JSON object or env.VAR_NAME</FormDescription>
<FormControl>
<EnvVarInput
data-testid="apikey-vertex-auth-credentials-input"
variant="textarea"
rows={4}
placeholder='{"type":"service_account","project_id":"your-gcp-project",...} or env.VERTEX_CREDENTIALS'
inputClassName="font-mono text-sm"
{...field}
/>
</FormControl>
{isRedacted(field.value?.value ?? "") && (
<div className="text-muted-foreground mt-1 flex items-center gap-1 text-xs">
<Info className="h-3 w-3" />
<span>Credentials are stored securely. Edit to update.</span>
</div>
)}
<FormMessage />
</FormItem>
)}
/>
)}
{vertexAuthType === "api_key" && (
<FormField
control={control}
name={`key.value`}
render={({ field }) => (
<FormItem>
<FormLabel>API Key (Supported only for gemini and fine-tuned models)</FormLabel>
<FormControl>
<EnvVarInput data-testid="apikey-vertex-api-key-input" placeholder="API Key or env.MY_KEY" type="text" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
)}
{isReplicate && (
<div className="space-y-4">
<Separator className="my-6" />
<FormField
control={control}
name="key.replicate_key_config.use_deployments_endpoint"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-sm border p-2">
<div className="space-y-1.5">
<FormLabel>Use Deployments Endpoint</FormLabel>
<FormDescription>
Route requests through the Replicate deployments endpoint instead of the models endpoint.
</FormDescription>
</div>
<FormControl>
<Switch checked={field.value ?? false} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)}
/>
</div>
)}
{isVLLM && (
<div className="space-y-4">
<Separator className="my-6" />
<FormField
control={control}
name="key.vllm_key_config.url"
render={({ field }) => (
<FormItem>
<FormLabel>Server URL (Required)</FormLabel>
<FormDescription>Base URL of the vLLM server (e.g. http://vllm-server:8000 or env.VLLM_URL)</FormDescription>
<FormControl>
<EnvVarInput data-testid="key-input-vllm-url" placeholder="http://vllm-server:8000" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name="key.vllm_key_config.model_name"
render={({ field }) => (
<FormItem>
<FormLabel>Model Name (Required)</FormLabel>
<FormDescription>Exact model name served on this vLLM instance</FormDescription>
<FormControl>
<Input data-testid="key-input-vllm-model-name" placeholder="meta-llama/Llama-3-70b-hf" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
{isKeylessProvider && (
<div className="space-y-4">
<FormField
control={control}
name={`key.${isOllama ? "ollama_key_config" : "sgl_key_config"}.url`}
render={({ field }) => (
<FormItem>
<FormLabel>Server URL (Required)</FormLabel>
<FormDescription>
Base URL of the {isOllama ? "Ollama" : "SGLang"} server (e.g.{" "}
{isOllama ? "http://localhost:11434" : "http://localhost:30000"} or {isOllama ? "env.OLLAMA_URL" : "env.SGL_URL"})
</FormDescription>
<FormControl>
<EnvVarInput
data-testid={`key-input-${isOllama ? "ollama" : "sgl"}-url`}
placeholder={isOllama ? "http://localhost:11434" : "http://localhost:30000"}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
{isBedrock && (
<div className="space-y-4">
<Separator className="my-6" />
<div className="space-y-2">
<FormLabel>Authentication Method</FormLabel>
<Tabs
value={bedrockAuthType}
onValueChange={(v) => {
setBedrockAuthType(v as "iam_role" | "explicit" | "api_key");
form.setValue("key.bedrock_key_config._auth_type", v, { shouldDirty: true, shouldValidate: true });
if (v === "iam_role") {
// Clear explicit credentials and API key when switching to IAM Role
form.setValue("key.bedrock_key_config.access_key", undefined, { shouldDirty: true });
form.setValue("key.bedrock_key_config.secret_key", undefined, { shouldDirty: true });
form.setValue("key.bedrock_key_config.session_token", undefined, { shouldDirty: true });
form.setValue("key.value", undefined, { shouldDirty: true });
} else if (v === "explicit") {
// Clear API key when switching to Explicit Credentials
form.setValue("key.value", undefined, { shouldDirty: true });
} else if (v === "api_key") {
// Clear AWS credentials and assume-role fields when switching to API Key
form.setValue("key.bedrock_key_config.access_key", undefined, { shouldDirty: true });
form.setValue("key.bedrock_key_config.secret_key", undefined, { shouldDirty: true });
form.setValue("key.bedrock_key_config.session_token", undefined, { shouldDirty: true });
form.setValue("key.bedrock_key_config.role_arn", undefined, { shouldDirty: true });
form.setValue("key.bedrock_key_config.external_id", undefined, { shouldDirty: true });
form.setValue("key.bedrock_key_config.session_name", undefined, { shouldDirty: true });
}
}}
>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger data-testid="apikey-bedrock-iam-role-tab" value="iam_role">
IAM Role (Inherited)
</TabsTrigger>
<TabsTrigger data-testid="apikey-bedrock-explicit-credentials-tab" value="explicit">
Explicit Credentials
</TabsTrigger>
<TabsTrigger data-testid="apikey-bedrock-api-key-tab" value="api_key">
API Key
</TabsTrigger>
</TabsList>
</Tabs>
{bedrockAuthType === "iam_role" && (
<p className="text-muted-foreground text-sm">Uses IAM roles attached to your environment (EC2, Lambda, ECS, EKS).</p>
)}
{bedrockAuthType === "api_key" && (
<p className="text-muted-foreground text-sm">Uses a Bearer token for API key authentication.</p>
)}
</div>
{bedrockAuthType === "explicit" && (
<>
<FormField
control={control}
name={`key.bedrock_key_config.access_key`}
render={({ field }) => (
<FormItem>
<FormLabel>Access Key (Required)</FormLabel>
<FormControl>
<EnvVarInput placeholder="your-aws-access-key or env.AWS_ACCESS_KEY_ID" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={`key.bedrock_key_config.secret_key`}
render={({ field }) => (
<FormItem>
<FormLabel>Secret Key (Required)</FormLabel>
<FormControl>
<EnvVarInput placeholder="your-aws-secret-key or env.AWS_SECRET_ACCESS_KEY" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={`key.bedrock_key_config.session_token`}
render={({ field }) => (
<FormItem>
<FormLabel>Session Token (Optional)</FormLabel>
<FormControl>
<EnvVarInput placeholder="your-aws-session-token or env.AWS_SESSION_TOKEN" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{bedrockAuthType === "api_key" && (
<FormField
control={control}
name={`key.value`}
render={({ field }) => (
<FormItem>
<FormLabel>API Key</FormLabel>
<FormControl>
<EnvVarInput
data-testid="apikey-bedrock-api-key-input"
placeholder="API Key or env.BEDROCK_API_KEY"
type="text"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={control}
name={`key.bedrock_key_config.region`}
render={({ field }) => (
<FormItem>
<FormLabel>Region (Required)</FormLabel>
<FormControl>
<EnvVarInput placeholder="us-east-1 or env.AWS_REGION" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{bedrockAuthType !== "api_key" && (
<>
<FormField
control={control}
name={`key.bedrock_key_config.role_arn`}
render={({ field }) => (
<FormItem>
<FormLabel>Assume Role ARN (Optional)</FormLabel>
<FormDescription>
Assume an IAM role before requests. Works with both explicit credentials and inherited IAM (EC2, ECS, EKS).
</FormDescription>
<FormControl>
<EnvVarInput
data-testid="apikey-bedrock-role-arn-input"
placeholder="arn:aws:iam::123456789:role/MyRole or env.AWS_ROLE_ARN"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={`key.bedrock_key_config.external_id`}
render={({ field }) => (
<FormItem>
<FormLabel>External ID (Optional)</FormLabel>
<FormDescription>Required by the role's trust policy when using cross-account access</FormDescription>
<FormControl>
<EnvVarInput
data-testid="apikey-bedrock-external-id-input"
placeholder="external-id or env.AWS_EXTERNAL_ID"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={control}
name={`key.bedrock_key_config.session_name`}
render={({ field }) => (
<FormItem>
<FormLabel>Session Name (Optional)</FormLabel>
<FormDescription>AssumeRole session name (defaults to bifrost-session)</FormDescription>
<FormControl>
<EnvVarInput
data-testid="apikey-bedrock-session-name-input"
placeholder="bifrost-session or env.AWS_SESSION_NAME"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
<FormField
control={control}
name={`key.bedrock_key_config.arn`}
render={({ field }) => (
<FormItem>
<FormLabel>ARN (Optional)</FormLabel>
<FormControl>
<EnvVarInput placeholder="arn:aws:bedrock:us-east-1:123:inference-profile or env.AWS_ARN" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{supportsBatchAPI && <BatchAPIFormField control={control} form={form} />}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,188 @@
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { getErrorMessage, setProviderFormDirtyState, useAppDispatch } from "@/lib/store";
import { useUpdateProviderMutation } from "@/lib/store/apis/providersApi";
import { BaseProvider, ModelProvider } from "@/lib/types/config";
import { formCustomProviderConfigSchema } from "@/lib/types/schemas";
import { cleanPathOverrides } from "@/lib/utils/validation";
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { buildProviderUpdatePayload } from "../views/utils";
import { AllowedRequestsFields } from "./allowedRequestsFields";
// Type for form data
type FormCustomProviderConfig = z.infer<typeof formCustomProviderConfigSchema>;
// Standalone usage (for provider configuration tabs)
interface Props {
provider: ModelProvider;
}
// Standalone component for provider configuration tabs
export function ApiStructureFormFragment({ provider }: Props) {
const hasUpdateProviderAccess = useRbac(RbacResource.ModelProvider, RbacOperation.Update);
const dispatch = useAppDispatch();
const [updateProvider, { isLoading: isUpdatingProvider }] = useUpdateProviderMutation();
const form = useForm<FormCustomProviderConfig>({
resolver: zodResolver(formCustomProviderConfigSchema),
mode: "onChange",
defaultValues: {
base_provider_type: provider.custom_provider_config?.base_provider_type ?? "openai",
is_key_less: provider.custom_provider_config?.is_key_less ?? false,
allowed_requests: {
text_completion: provider.custom_provider_config?.allowed_requests?.text_completion ?? true,
text_completion_stream: provider.custom_provider_config?.allowed_requests?.text_completion_stream ?? true,
chat_completion: provider.custom_provider_config?.allowed_requests?.chat_completion ?? true,
chat_completion_stream: provider.custom_provider_config?.allowed_requests?.chat_completion_stream ?? true,
responses: provider.custom_provider_config?.allowed_requests?.responses ?? true,
responses_stream: provider.custom_provider_config?.allowed_requests?.responses_stream ?? true,
embedding: provider.custom_provider_config?.allowed_requests?.embedding ?? true,
speech: provider.custom_provider_config?.allowed_requests?.speech ?? true,
speech_stream: provider.custom_provider_config?.allowed_requests?.speech_stream ?? true,
transcription: provider.custom_provider_config?.allowed_requests?.transcription ?? true,
transcription_stream: provider.custom_provider_config?.allowed_requests?.transcription_stream ?? true,
count_tokens: provider.custom_provider_config?.allowed_requests?.count_tokens ?? true,
list_models: provider.custom_provider_config?.allowed_requests?.list_models ?? true,
ocr: provider.custom_provider_config?.allowed_requests?.ocr ?? true,
ocr_stream: provider.custom_provider_config?.allowed_requests?.ocr_stream ?? true,
},
request_path_overrides: provider.custom_provider_config?.request_path_overrides ?? undefined,
},
});
useEffect(() => {
dispatch(setProviderFormDirtyState(form.formState.isDirty));
}, [form.formState.isDirty]);
useEffect(() => {
form.reset(provider.custom_provider_config);
}, [form, provider.name, provider.custom_provider_config]);
const onSubmit = (data: FormCustomProviderConfig) => {
// Create updated provider configuration
updateProvider(
buildProviderUpdatePayload(provider, {
custom_provider_config: {
base_provider_type: data.base_provider_type as unknown as BaseProvider,
is_key_less: data.is_key_less ?? false,
allowed_requests: data.allowed_requests,
request_path_overrides: cleanPathOverrides(data.request_path_overrides),
},
}),
)
.unwrap()
.then(() => {
toast.success("Provider configuration updated successfully");
form.reset(data);
})
.catch((err) => {
toast.error("Failed to update provider configuration", {
description: getErrorMessage(err),
});
});
};
const isKeyLessDisabled = useMemo(
() => provider.custom_provider_config?.base_provider_type === "bedrock",
[provider.custom_provider_config?.base_provider_type],
);
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 px-6">
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="base_provider_type"
render={({ field }) => (
<FormItem>
<FormLabel>Base Provider Type</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger disabled={true}>
<SelectValue placeholder="Select base provider" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="openai">OpenAI</SelectItem>
<SelectItem value="anthropic">Anthropic</SelectItem>
<SelectItem value="bedrock">AWS Bedrock</SelectItem>
<SelectItem value="cohere">Cohere</SelectItem>
<SelectItem value="gemini">Gemini</SelectItem>
<SelectItem value="replicate">Replicate</SelectItem>
</SelectContent>
</Select>
<FormDescription>The underlying provider this custom provider will use</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{!isKeyLessDisabled && (
<FormField
control={form.control}
name="is_key_less"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between space-x-2 rounded-lg border p-3">
<div className="space-y-0.5">
<label htmlFor="drop-excess-requests" className="text-sm font-medium">
Is Keyless?
</label>
<p className="text-muted-foreground text-sm">Whether the custom provider requires a key</p>
</div>
<Switch
id="drop-excess-requests"
size="md"
checked={field.value}
onCheckedChange={field.onChange}
disabled={!hasUpdateProviderAccess}
/>
</div>
</FormItem>
)}
/>
)}
</div>
{/* Allowed Requests Configuration */}
<AllowedRequestsFields
control={form.control}
providerType={form.watch("base_provider_type") as BaseProvider}
disabled={!hasUpdateProviderAccess}
/>
{/* Form Actions */}
<div className="flex justify-end space-x-2 py-2">
<Button type="button" variant="outline" onClick={() => form.reset()} disabled={!hasUpdateProviderAccess}>
Reset
</Button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="submit"
disabled={!form.formState.isDirty || !form.formState.isValid || !hasUpdateProviderAccess}
isLoading={isUpdatingProvider}
>
Save API Structure Configuration
</Button>
</TooltipTrigger>
{!form.formState.isValid && (
<TooltipContent>
<p>{form.formState.errors.root?.message || "Please fix validation errors"}</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
</div>
</form>
</Form>
);
}

View File

@@ -0,0 +1,418 @@
import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { getErrorMessage, setProviderFormDirtyState, useAppDispatch } from "@/lib/store";
import { useUpdateProviderMutation } from "@/lib/store/apis/providersApi";
import { ModelProvider, NetworkConfig } from "@/lib/types/config";
import { buildProviderUpdatePayload } from "@/app/workspace/providers/views/utils";
import { betaHeadersFormSchema, type BetaHeadersFormSchema } from "@/lib/types/schemas";
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
import { zodResolver } from "@hookform/resolvers/zod";
import { Plus, Trash2 } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useForm, type Resolver } from "react-hook-form";
import { toast } from "sonner";
// Known beta headers with their prefixes, descriptions, and default support per provider.
// This mirrors the Go ProviderFeatures map in core/providers/anthropic/types.go.
const KNOWN_BETA_HEADERS = [
{
prefix: "computer-use-",
label: "Computer Use",
description: "Computer use client tool",
defaults: { anthropic: true, vertex: true, bedrock: true, azure: true },
},
{
prefix: "structured-outputs-",
label: "Structured Outputs",
description: "Strict tool validation and output_format",
defaults: { anthropic: true, vertex: false, bedrock: true, azure: true },
},
{
prefix: "advanced-tool-use-",
label: "Advanced Tool Use",
description: "defer_loading, input_examples, allowed_callers",
defaults: { anthropic: true, vertex: false, bedrock: false, azure: true },
},
{
prefix: "mcp-client-",
label: "MCP Client",
description: "MCP connector support",
defaults: { anthropic: true, vertex: false, bedrock: false, azure: true },
},
{
prefix: "prompt-caching-scope-",
label: "Prompt Caching Scope",
description: "Prompt caching scope control",
defaults: { anthropic: true, vertex: false, bedrock: false, azure: true },
},
{
prefix: "compact-",
label: "Compaction",
description: "Server-side context compaction",
defaults: { anthropic: true, vertex: true, bedrock: true, azure: true },
},
{
prefix: "context-management-",
label: "Context Management",
description: "Context editing (clear_tool_uses, clear_thinking)",
defaults: { anthropic: true, vertex: true, bedrock: true, azure: true },
},
{
prefix: "files-api-",
label: "Files API",
description: "Files API support",
defaults: { anthropic: true, vertex: false, bedrock: false, azure: true },
},
{
prefix: "interleaved-thinking-",
label: "Interleaved Thinking",
description: "Interleaved thinking between tool calls",
defaults: { anthropic: true, vertex: true, bedrock: true, azure: true },
},
{
prefix: "skills-",
label: "Skills",
description: "Agent Skills",
defaults: { anthropic: true, vertex: false, bedrock: false, azure: true },
},
{
prefix: "context-1m-",
label: "Context 1M",
description: "1M context window (beta for Sonnet 4.5/4)",
defaults: { anthropic: true, vertex: true, bedrock: true, azure: true },
},
{
prefix: "fast-mode-",
label: "Fast Mode",
description: "Fast mode (Opus 4.6 research preview)",
defaults: { anthropic: true, vertex: false, bedrock: false, azure: false },
},
{
prefix: "redact-thinking-",
label: "Redact Thinking",
description: "Redact thinking blocks in responses",
defaults: { anthropic: true, vertex: false, bedrock: false, azure: true },
},
] as const;
const KNOWN_PREFIXES = new Set<string>(KNOWN_BETA_HEADERS.map((h) => h.prefix));
type ProviderKey = "anthropic" | "vertex" | "bedrock" | "azure";
const ANTHROPIC_FAMILY_PROVIDERS: ProviderKey[] = ["anthropic", "vertex", "bedrock", "azure"];
function getProviderKey(providerName: string): ProviderKey | null {
const name = providerName.toLowerCase();
if (ANTHROPIC_FAMILY_PROVIDERS.includes(name as ProviderKey)) {
return name as ProviderKey;
}
return null;
}
interface BetaHeadersFormFragmentProps {
provider: ModelProvider;
}
export function BetaHeadersFormFragment({ provider }: BetaHeadersFormFragmentProps) {
const dispatch = useAppDispatch();
const hasUpdateProviderAccess = useRbac(RbacResource.ModelProvider, RbacOperation.Update);
const [updateProvider, { isLoading: isUpdatingProvider }] = useUpdateProviderMutation();
const providerKey = getProviderKey(provider.name);
const [newPrefix, setNewPrefix] = useState("");
const [newPrefixError, setNewPrefixError] = useState<string | null>(null);
const form = useForm<BetaHeadersFormSchema, any, BetaHeadersFormSchema>({
resolver: zodResolver(betaHeadersFormSchema) as Resolver<BetaHeadersFormSchema, any, BetaHeadersFormSchema>,
mode: "onChange",
reValidateMode: "onChange",
defaultValues: {
beta_header_overrides: provider.network_config?.beta_header_overrides ?? {},
},
});
useEffect(() => {
form.reset({
beta_header_overrides: provider.network_config?.beta_header_overrides ?? {},
});
}, [form, provider.name, provider.network_config?.beta_header_overrides]);
const overrides = form.watch("beta_header_overrides") ?? {};
// Manual dirty tracking — RHF's deep equality on records is unreliable with setValue
const savedOverrides = provider.network_config?.beta_header_overrides ?? {};
const isManuallyDirty = useMemo(() => {
const currentKeys = Object.keys(overrides);
const savedKeys = Object.keys(savedOverrides);
if (currentKeys.length !== savedKeys.length) return true;
return currentKeys.some((key) => overrides[key] !== savedOverrides[key]);
}, [overrides, savedOverrides]);
useEffect(() => {
dispatch(setProviderFormDirtyState(isManuallyDirty));
}, [isManuallyDirty, dispatch]);
// Custom prefixes are overrides that don't match any known prefix
const customPrefixes = useMemo(() => {
return Object.keys(overrides).filter((prefix) => !KNOWN_PREFIXES.has(prefix));
}, [overrides]);
const headerRows = useMemo(() => {
if (!providerKey) return [];
return KNOWN_BETA_HEADERS.map((header) => {
const defaultSupported = header.defaults[providerKey];
const override = overrides[header.prefix];
return { ...header, defaultSupported, override };
});
}, [providerKey, overrides]);
const onSubmit = (data: BetaHeadersFormSchema) => {
const cleanedOverrides: Record<string, boolean> = {};
if (data.beta_header_overrides) {
for (const [prefix, value] of Object.entries(data.beta_header_overrides)) {
cleanedOverrides[prefix] = value;
}
}
updateProvider(
buildProviderUpdatePayload(provider, {
network_config: {
...(provider.network_config ?? ({} as NetworkConfig)),
beta_header_overrides: Object.keys(cleanedOverrides).length > 0 ? cleanedOverrides : undefined,
},
}),
)
.unwrap()
.then(() => {
toast.success("Beta header configuration updated successfully");
form.reset(data);
})
.catch((err) => {
toast.error("Failed to update beta header configuration", {
description: getErrorMessage(err),
});
});
};
const setOverride = useCallback(
(prefix: string, value: "default" | "enabled" | "disabled") => {
const current = form.getValues("beta_header_overrides") ?? {};
const updated = { ...current };
if (value === "default") {
delete updated[prefix];
} else {
updated[prefix] = value === "enabled";
}
form.setValue("beta_header_overrides", updated, { shouldDirty: true });
},
[form],
);
const removeCustomPrefix = useCallback(
(prefix: string) => {
const current = form.getValues("beta_header_overrides") ?? {};
const updated = { ...current };
delete updated[prefix];
form.setValue("beta_header_overrides", updated, { shouldDirty: true });
},
[form],
);
const addCustomPrefix = useCallback(() => {
let prefix = newPrefix.trim().toLowerCase();
if (!prefix) return;
// Ensure prefix ends with "-"
if (!prefix.endsWith("-")) {
prefix = prefix + "-";
}
// Validate
if (KNOWN_PREFIXES.has(prefix)) {
setNewPrefixError("This is a known header — use the override dropdown above instead");
return;
}
if (overrides[prefix] !== undefined) {
setNewPrefixError("This prefix already exists");
return;
}
if (!/^[a-z0-9-]+$/.test(prefix)) {
setNewPrefixError("Prefix must contain only lowercase letters, numbers, and hyphens");
return;
}
const current = form.getValues("beta_header_overrides") ?? {};
form.setValue("beta_header_overrides", { ...current, [prefix]: true }, { shouldDirty: true });
setNewPrefix("");
setNewPrefixError(null);
}, [newPrefix, overrides, form]);
const getSelectValue = (prefix: string): string => {
const override = overrides[prefix];
if (override === undefined) return "default";
return override ? "enabled" : "disabled";
};
if (!providerKey) return null;
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 px-6" data-testid="provider-config-beta-headers-content">
<div className="space-y-2">
<p className="text-muted-foreground text-xs">
Configure which Anthropic beta headers are allowed for this provider. Override the defaults when a provider adds or removes
support for a beta feature.
</p>
<div className="rounded-md border">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="px-3 py-2 text-left font-medium">Beta Header</th>
<th className="px-3 py-2 text-left font-medium">Default</th>
<th className="w-[180px] px-3 py-2 text-left font-medium">Override</th>
</tr>
</thead>
<tbody>
{headerRows.map((row) => (
<tr key={row.prefix} className="border-b last:border-b-0">
<td className="px-3 py-2">
<div className="flex flex-col gap-0.5">
<span className="font-mono text-xs">{row.prefix}*</span>
<span className="text-muted-foreground text-xs">{row.description}</span>
</div>
</td>
<td className="px-3 py-2">
<Badge variant={row.defaultSupported ? "default" : "secondary"} className="text-xs">
{row.defaultSupported ? "Supported" : "Unsupported"}
</Badge>
</td>
<td className="w-[180px] px-3 py-2">
<Select
value={getSelectValue(row.prefix)}
onValueChange={(val) => setOverride(row.prefix, val as "default" | "enabled" | "disabled")}
disabled={!hasUpdateProviderAccess}
>
<SelectTrigger
className="h-8 text-xs"
data-testid={`provider-beta-override-select-${row.prefix.replace(/-/g, "")}`}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="default">Default</SelectItem>
<SelectItem value="enabled">Supported</SelectItem>
<SelectItem value="disabled">Unsupported</SelectItem>
</SelectContent>
</Select>
</td>
</tr>
))}
{customPrefixes.map((prefix) => (
<tr key={prefix} className="border-b last:border-b-0">
<td className="px-3 py-2">
<div className="flex flex-col gap-0.5">
<span className="font-mono text-xs">{prefix}*</span>
<span className="text-muted-foreground text-xs">Custom header</span>
</div>
</td>
<td className="px-3 py-2">
<Badge variant="outline" className="text-xs">
Custom
</Badge>
</td>
<td className="w-[180px] px-3 py-2">
<div className="flex items-center gap-1">
<Select
value={overrides[prefix] ? "enabled" : "disabled"}
onValueChange={(val) => setOverride(prefix, val as "enabled" | "disabled")}
disabled={!hasUpdateProviderAccess}
>
<SelectTrigger
className="h-8 text-xs"
data-testid={`provider-beta-custom-override-select-${prefix.replace(/-/g, "")}`}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="enabled">Supported</SelectItem>
<SelectItem value="disabled">Unsupported</SelectItem>
</SelectContent>
</Select>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0"
disabled={!hasUpdateProviderAccess}
onClick={() => removeCustomPrefix(prefix)}
data-testid={`provider-beta-remove-prefix-btn-${prefix.replace(/-/g, "")}`}
aria-label={`Remove custom prefix ${prefix}`}
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex items-start gap-2 pt-2">
<div className="flex-1">
<Input
placeholder="Add custom beta header prefix (e.g. new-feature-)"
value={newPrefix}
onChange={(e) => {
setNewPrefix(e.target.value);
setNewPrefixError(null);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
addCustomPrefix();
}
}}
disabled={!hasUpdateProviderAccess}
className="h-8 text-xs"
data-testid="provider-beta-custom-prefix-input"
aria-label="Custom beta header prefix"
aria-describedby={newPrefixError ? "custom-prefix-error" : undefined}
/>
{newPrefixError && (
<p className="text-destructive mt-1 text-xs" id="custom-prefix-error">
{newPrefixError}
</p>
)}
</div>
<Button
type="button"
variant="outline"
size="sm"
className="h-8"
disabled={!hasUpdateProviderAccess || !newPrefix.trim()}
onClick={addCustomPrefix}
data-testid="provider-beta-add-prefix-btn"
>
<Plus className="mr-1 h-3.5 w-3.5" />
Add
</Button>
</div>
</div>
<div className="flex justify-end space-x-2 pb-6">
<Button
type="submit"
disabled={!isManuallyDirty || !hasUpdateProviderAccess || isUpdatingProvider}
isLoading={isUpdatingProvider}
data-testid="provider-beta-save-btn"
>
Save Beta Header Configuration
</Button>
</div>
</form>
</Form>
);
}

View File

@@ -0,0 +1,214 @@
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { getErrorMessage, setProviderFormDirtyState, useAppDispatch } from "@/lib/store";
import { useUpdateProviderMutation } from "@/lib/store/apis/providersApi";
import { ModelProvider } from "@/lib/types/config";
import { debuggingFormSchema, type DebuggingFormSchema } from "@/lib/types/schemas";
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
import { zodResolver } from "@hookform/resolvers/zod";
import { Info } from "lucide-react";
import { useEffect } from "react";
import { useForm, type Resolver } from "react-hook-form";
import { toast } from "sonner";
import { buildProviderUpdatePayload } from "../views/utils";
interface DebuggingFormFragmentProps {
provider: ModelProvider;
}
export function DebuggingFormFragment({ provider }: DebuggingFormFragmentProps) {
const dispatch = useAppDispatch();
const hasUpdateProviderAccess = useRbac(RbacResource.ModelProvider, RbacOperation.Update);
const [updateProvider, { isLoading: isUpdatingProvider }] = useUpdateProviderMutation();
const form = useForm<DebuggingFormSchema, any, DebuggingFormSchema>({
resolver: zodResolver(debuggingFormSchema) as Resolver<DebuggingFormSchema, any, DebuggingFormSchema>,
mode: "onChange",
reValidateMode: "onChange",
defaultValues: {
send_back_raw_request: provider.send_back_raw_request ?? false,
send_back_raw_response: provider.send_back_raw_response ?? false,
store_raw_request_response: provider.store_raw_request_response ?? false,
},
});
const sendBackRawRequest = form.watch("send_back_raw_request");
const sendBackRawResponse = form.watch("send_back_raw_response");
const storeRawRequestResponse = form.watch("store_raw_request_response");
useEffect(() => {
dispatch(setProviderFormDirtyState(form.formState.isDirty));
}, [form.formState.isDirty, dispatch]);
useEffect(() => {
form.reset({
send_back_raw_request: provider.send_back_raw_request ?? false,
send_back_raw_response: provider.send_back_raw_response ?? false,
store_raw_request_response: provider.store_raw_request_response ?? false,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [provider.name, provider.send_back_raw_request, provider.send_back_raw_response, provider.store_raw_request_response]);
const onSubmit = (data: DebuggingFormSchema) => {
const updatedProvider = buildProviderUpdatePayload(provider, {
send_back_raw_request: data.send_back_raw_request,
send_back_raw_response: data.send_back_raw_response,
store_raw_request_response: data.store_raw_request_response,
});
updateProvider(updatedProvider)
.unwrap()
.then(() => {
toast.success("Debugging configuration updated successfully");
form.reset(data);
})
.catch((err) => {
toast.error("Failed to update debugging configuration", {
description: getErrorMessage(err),
});
});
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 px-6" data-testid="provider-config-debugging-content">
<div className="space-y-4">
{/* Send Back Raw Request */}
<FormField
control={form.control}
name="send_back_raw_request"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between space-x-2">
<div className="space-y-0.5">
<div className="flex items-center gap-1.5">
<FormLabel>Send Back Raw Request</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild data-testid="provider-debugging-send-back-raw-request-tooltip-trigger">
<Info className="text-muted-foreground h-3 w-3 cursor-pointer" />
</TooltipTrigger>
<TooltipContent>
Override per-request with header: <code>x-bf-send-back-raw-request: {String(!sendBackRawRequest)}</code>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<p className="text-muted-foreground text-xs">
Include the raw provider request alongside the parsed request in the API response.
</p>
</div>
<FormControl>
<Switch
size="md"
checked={field.value}
disabled={!hasUpdateProviderAccess}
onCheckedChange={(checked) => {
field.onChange(checked);
form.trigger("send_back_raw_request");
}}
/>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* Send Back Raw Response */}
<FormField
control={form.control}
name="send_back_raw_response"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between space-x-2">
<div className="space-y-0.5">
<div className="flex items-center gap-1.5">
<FormLabel>Send Back Raw Response</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild data-testid="provider-debugging-send-back-raw-response-tooltip-trigger">
<Info className="text-muted-foreground h-3 w-3 cursor-pointer" />
</TooltipTrigger>
<TooltipContent>
Override per-request with header: <code>x-bf-send-back-raw-response: {String(!sendBackRawResponse)}</code>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<p className="text-muted-foreground text-xs">
Include the raw provider response alongside the parsed response in the API response.
</p>
</div>
<FormControl>
<Switch
size="md"
checked={field.value}
disabled={!hasUpdateProviderAccess}
onCheckedChange={(checked) => {
field.onChange(checked);
form.trigger("send_back_raw_response");
}}
/>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* Store Raw Request/Response */}
<FormField
control={form.control}
name="store_raw_request_response"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between space-x-2">
<div className="space-y-0.5">
<div className="flex items-center gap-1.5">
<FormLabel>Store Raw Request/Response</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild data-testid="provider-debugging-store-raw-request-response-tooltip-trigger">
<Info className="text-muted-foreground h-3 w-3 cursor-pointer" />
</TooltipTrigger>
<TooltipContent>
Override per-request with header:{" "}
<code>x-bf-store-raw-request-response: {String(!storeRawRequestResponse)}</code>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<p className="text-muted-foreground text-xs">Persist raw request and response payloads in log records.</p>
</div>
<FormControl>
<Switch
data-testid="provider-debugging-store-raw-request-response-switch"
size="md"
checked={field.value}
disabled={!hasUpdateProviderAccess}
onCheckedChange={(checked) => {
field.onChange(checked);
form.trigger("store_raw_request_response");
}}
/>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end space-x-2 pb-6">
<Button
type="submit"
disabled={!form.formState.isDirty || !form.formState.isValid || !hasUpdateProviderAccess || isUpdatingProvider}
isLoading={isUpdatingProvider}
>
Save Debugging Configuration
</Button>
</div>
</form>
</Form>
);
}

View File

@@ -0,0 +1,299 @@
import { Button } from "@/components/ui/button";
import { Form, FormField, FormItem } from "@/components/ui/form";
import { Label } from "@/components/ui/label";
import NumberAndSelect from "@/components/ui/numberAndSelect";
import { DottedSeparator } from "@/components/ui/separator";
import { resetDurationOptions } from "@/lib/constants/governance";
import {
getErrorMessage,
useDeleteProviderGovernanceMutation,
useGetProviderGovernanceQuery,
useUpdateProviderGovernanceMutation,
} from "@/lib/store";
import { ModelProvider } from "@/lib/types/config";
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
interface GovernanceFormFragmentProps {
provider: ModelProvider;
}
const formSchema = z.object({
// Budget
budgetMaxLimit: z.number().nonnegative().optional(),
budgetResetDuration: z.string().optional(),
// Token limits
tokenMaxLimit: z.number().int().nonnegative().optional(),
tokenResetDuration: z.string().optional(),
// Request limits
requestMaxLimit: z.number().int().nonnegative().optional(),
requestResetDuration: z.string().optional(),
});
type FormData = z.infer<typeof formSchema>;
const DEFAULT_GOVERNANCE_FORM_VALUES: FormData = {
budgetMaxLimit: undefined,
budgetResetDuration: "1M",
tokenMaxLimit: undefined,
tokenResetDuration: "1h",
requestMaxLimit: undefined,
requestResetDuration: "1h",
};
export function GovernanceFormFragment({ provider }: GovernanceFormFragmentProps) {
const hasUpdateProviderAccess = useRbac(RbacResource.ModelProvider, RbacOperation.Update);
const hasViewAccess = useRbac(RbacResource.Governance, RbacOperation.View);
const { data: providerGovernanceData } = useGetProviderGovernanceQuery(undefined, {
skip: !hasViewAccess,
pollingInterval: 5000,
});
const [updateProviderGovernance, { isLoading: isUpdating }] = useUpdateProviderGovernanceMutation();
const [deleteProviderGovernance, { isLoading: isDeleting }] = useDeleteProviderGovernanceMutation();
// Find governance data for this provider
const providerGovernance = providerGovernanceData?.providers?.find((p) => p.provider === provider.name);
const hasExistingGovernance = !!(providerGovernance?.budget || providerGovernance?.rate_limit);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: DEFAULT_GOVERNANCE_FORM_VALUES,
});
// Update form values when provider governance data is loaded (polling)
useEffect(() => {
// Never reset form during polling if user is editing
if (providerGovernance && !form.formState.isDirty) {
form.reset({
budgetMaxLimit: providerGovernance.budget?.max_limit ?? undefined,
budgetResetDuration: providerGovernance.budget?.reset_duration || "1M",
tokenMaxLimit: providerGovernance.rate_limit?.token_max_limit ?? undefined,
tokenResetDuration: providerGovernance.rate_limit?.token_reset_duration || "1h",
requestMaxLimit: providerGovernance.rate_limit?.request_max_limit ?? undefined,
requestResetDuration: providerGovernance.rate_limit?.request_reset_duration || "1h",
});
}
}, [providerGovernance, form]);
// Reset form when provider changes
useEffect(() => {
// Never reset form if user is editing - just skip the reset
if (form.formState.isDirty) {
return;
}
const newProvGov = providerGovernanceData?.providers?.find((p) => p.provider === provider.name);
form.reset({
budgetMaxLimit: newProvGov?.budget?.max_limit ?? undefined,
budgetResetDuration: newProvGov?.budget?.reset_duration || "1M",
tokenMaxLimit: newProvGov?.rate_limit?.token_max_limit ?? undefined,
tokenResetDuration: newProvGov?.rate_limit?.token_reset_duration || "1h",
requestMaxLimit: newProvGov?.rate_limit?.request_max_limit ?? undefined,
requestResetDuration: newProvGov?.rate_limit?.request_reset_duration || "1h",
});
}, [provider.name, form]);
const onSubmit = async (data: FormData) => {
try {
// Determine if we need to send empty objects to signal removal
const hadBudget = !!providerGovernance?.budget;
const hasBudget = data.budgetMaxLimit !== undefined;
const hadRateLimit = !!providerGovernance?.rate_limit;
const hasRateLimit = data.tokenMaxLimit !== undefined || data.requestMaxLimit !== undefined;
let budgetPayload: { max_limit?: number; reset_duration?: string } | undefined;
if (hasBudget) {
budgetPayload = {
max_limit: data.budgetMaxLimit,
reset_duration: data.budgetResetDuration || "1M",
};
} else if (hadBudget) {
budgetPayload = {};
}
let rateLimitPayload:
| {
token_max_limit?: number | null;
token_reset_duration?: string | null;
request_max_limit?: number | null;
request_reset_duration?: string | null;
}
| undefined;
if (hasRateLimit) {
rateLimitPayload = {
token_max_limit: data.tokenMaxLimit ?? null,
token_reset_duration: data.tokenMaxLimit !== undefined ? data.tokenResetDuration || "1h" : null,
request_max_limit: data.requestMaxLimit ?? null,
request_reset_duration: data.requestMaxLimit !== undefined ? data.requestResetDuration || "1h" : null,
};
} else if (hadRateLimit) {
rateLimitPayload = {};
}
await updateProviderGovernance({
provider: provider.name,
data: {
budget: budgetPayload,
rate_limit: rateLimitPayload,
},
}).unwrap();
toast.success("Governance configuration saved successfully");
// Reset form with the saved values to update the initial state for change detection
form.reset(data);
} catch (error) {
toast.error("Failed to update provider governance", {
description: getErrorMessage(error),
});
}
};
const handleDelete = async () => {
try {
await deleteProviderGovernance(provider.name).unwrap();
toast.success("Governance removed successfully");
form.reset(DEFAULT_GOVERNANCE_FORM_VALUES);
} catch (error) {
toast.error("Failed to remove governance", {
description: getErrorMessage(error),
});
}
};
// Always show the form
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 px-6">
{/* Budget Configuration */}
<div className="space-y-4">
<Label className="text-sm font-medium">Budget Configuration</Label>
<FormField
control={form.control}
name="budgetMaxLimit"
render={({ field }) => (
<FormItem>
<NumberAndSelect
id="providerBudgetMaxLimit"
labelClassName="font-normal"
label="Maximum Spend (USD)"
value={field.value}
selectValue={form.watch("budgetResetDuration") || "1M"}
onChangeNumber={(value) => field.onChange(value)}
onChangeSelect={(value) => form.setValue("budgetResetDuration", value, { shouldDirty: true })}
options={resetDurationOptions}
/>
</FormItem>
)}
/>
</div>
<DottedSeparator />
{/* Rate Limiting Configuration */}
<div className="space-y-4">
<Label className="text-sm font-medium">Rate Limiting Configuration</Label>
<FormField
control={form.control}
name="tokenMaxLimit"
render={({ field }) => (
<FormItem>
<NumberAndSelect
id="providerTokenMaxLimit"
labelClassName="font-normal"
label="Maximum Tokens"
value={field.value}
selectValue={form.watch("tokenResetDuration") || "1h"}
onChangeNumber={(value) => field.onChange(value)}
onChangeSelect={(value) => form.setValue("tokenResetDuration", value, { shouldDirty: true })}
options={resetDurationOptions}
/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="requestMaxLimit"
render={({ field }) => (
<FormItem>
<NumberAndSelect
id="providerRequestMaxLimit"
labelClassName="font-normal"
label="Maximum Requests"
value={field.value}
selectValue={form.watch("requestResetDuration") || "1h"}
onChangeNumber={(value) => field.onChange(value)}
onChangeSelect={(value) => form.setValue("requestResetDuration", value, { shouldDirty: true })}
options={resetDurationOptions}
/>
</FormItem>
)}
/>
</div>
{/* Current Usage Display - only when editing existing */}
{hasExistingGovernance && (providerGovernance?.budget || providerGovernance?.rate_limit) && (
<>
<DottedSeparator />
<div className="space-y-4">
<Label className="text-sm font-medium">Current Usage</Label>
<div className="bg-muted/50 grid grid-cols-2 gap-4 rounded-lg p-4">
{providerGovernance?.budget && (
<div className="space-y-1">
<p className="text-muted-foreground text-xs">Budget Usage</p>
<p className="text-sm font-medium">
${providerGovernance.budget.current_usage.toFixed(2)} / ${providerGovernance.budget.max_limit.toFixed(2)}
</p>
</div>
)}
{providerGovernance?.rate_limit?.token_max_limit && (
<div className="space-y-1">
<p className="text-muted-foreground text-xs">Token Usage</p>
<p className="text-sm font-medium">
{providerGovernance.rate_limit.token_current_usage.toLocaleString()} /{" "}
{providerGovernance.rate_limit.token_max_limit.toLocaleString()}
</p>
</div>
)}
{providerGovernance?.rate_limit?.request_max_limit && (
<div className="space-y-1">
<p className="text-muted-foreground text-xs">Request Usage</p>
<p className="text-sm font-medium">
{providerGovernance.rate_limit.request_current_usage.toLocaleString()} /{" "}
{providerGovernance.rate_limit.request_max_limit.toLocaleString()}
</p>
</div>
)}
</div>
</div>
</>
)}
{/* Form Actions */}
<div className="flex justify-end space-x-2 pb-6">
<Button
type="button"
variant="outline"
onClick={handleDelete}
disabled={!hasUpdateProviderAccess || isDeleting || !hasExistingGovernance}
>
Remove configuration
</Button>
<Button
type="submit"
disabled={!form.formState.isDirty || !form.formState.isValid || !hasUpdateProviderAccess || isUpdating}
isLoading={isUpdating}
>
Save Governance Configuration
</Button>
</div>
</form>
</Form>
);
}

View File

@@ -0,0 +1,11 @@
export { AllowedRequestsFields } from "./allowedRequestsFields";
export { BetaHeadersFormFragment } from "./betaHeadersFormFragment";
export { ApiKeyFormFragment } from "./apiKeysFormFragment";
export { ApiStructureFormFragment } from "./apiStructureFormFragment";
export { DebuggingFormFragment } from "./debuggingFormFragment";
export { GovernanceFormFragment } from "./governanceFormFragment";
export { OpenAIConfigFormFragment } from "./openaiConfigFormFragment";
export { NetworkFormFragment } from "./networkFormFragment";
export { PerformanceFormFragment } from "./performanceFormFragment";
export { PerformanceFormFragment as PerformanceTab } from "./performanceFormFragment";
export { ProxyFormFragment } from "./proxyFormFragment";

View File

@@ -0,0 +1,522 @@
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { HeadersTable } from "@/components/ui/headersTable";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { DefaultNetworkConfig } from "@/lib/constants/config";
import { getErrorMessage, setProviderFormDirtyState, useAppDispatch } from "@/lib/store";
import { useUpdateProviderMutation } from "@/lib/store/apis/providersApi";
import { ModelProvider, isKnownProvider } from "@/lib/types/config";
import { networkOnlyFormSchema, type NetworkOnlyFormSchema } from "@/lib/types/schemas";
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm, type Resolver } from "react-hook-form";
import { toast } from "sonner";
import { buildProviderUpdatePayload } from "../views/utils";
interface NetworkFormFragmentProps {
provider: ModelProvider;
}
// seconds to human readable time
const secondsToHumanReadable = (seconds: number) => {
// Handle edge cases
if (!seconds || seconds < 0 || isNaN(seconds)) {
return "0 seconds";
}
seconds = Math.floor(seconds);
if (seconds < 60) {
return `${seconds} ${seconds === 1 ? "second" : "seconds"}`;
}
if (seconds < 3600) {
const minutes = Math.floor(seconds / 60);
return `${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
}
if (seconds < 86400) {
const hours = Math.floor(seconds / 3600);
return `${hours} ${hours === 1 ? "hour" : "hours"}`;
}
// For >= 1 day, only show non-zero components
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
const parts: string[] = [];
parts.push(`${days} ${days === 1 ? "day" : "days"}`);
if (hours > 0) parts.push(`${hours} ${hours === 1 ? "hour" : "hours"}`);
if (minutes > 0) parts.push(`${minutes} ${minutes === 1 ? "minute" : "minutes"}`);
if (remainingSeconds > 0) parts.push(`${remainingSeconds} ${remainingSeconds === 1 ? "second" : "seconds"}`);
return parts.join(" ");
};
export function NetworkFormFragment({ provider }: NetworkFormFragmentProps) {
const dispatch = useAppDispatch();
const hasUpdateProviderAccess = useRbac(RbacResource.ModelProvider, RbacOperation.Update);
const [updateProvider, { isLoading: isUpdatingProvider }] = useUpdateProviderMutation();
const isCustomProvider = !isKnownProvider(provider.name as string);
const form = useForm<NetworkOnlyFormSchema, any, NetworkOnlyFormSchema>({
resolver: zodResolver(networkOnlyFormSchema) as Resolver<NetworkOnlyFormSchema, any, NetworkOnlyFormSchema>,
mode: "onChange",
reValidateMode: "onChange",
defaultValues: {
network_config: {
base_url: provider.network_config?.base_url || undefined,
extra_headers: provider.network_config?.extra_headers,
default_request_timeout_in_seconds:
provider.network_config?.default_request_timeout_in_seconds ?? DefaultNetworkConfig.default_request_timeout_in_seconds,
max_retries: provider.network_config?.max_retries ?? DefaultNetworkConfig.max_retries,
retry_backoff_initial: provider.network_config?.retry_backoff_initial ?? DefaultNetworkConfig.retry_backoff_initial,
retry_backoff_max: provider.network_config?.retry_backoff_max ?? DefaultNetworkConfig.retry_backoff_max,
insecure_skip_verify: provider.network_config?.insecure_skip_verify ?? DefaultNetworkConfig.insecure_skip_verify,
ca_cert_pem: provider.network_config?.ca_cert_pem ?? DefaultNetworkConfig.ca_cert_pem,
stream_idle_timeout_in_seconds:
provider.network_config?.stream_idle_timeout_in_seconds ?? DefaultNetworkConfig.stream_idle_timeout_in_seconds,
max_conns_per_host: provider.network_config?.max_conns_per_host ?? DefaultNetworkConfig.max_conns_per_host,
enforce_http2: provider.network_config?.enforce_http2 ?? DefaultNetworkConfig.enforce_http2,
},
},
});
useEffect(() => {
dispatch(setProviderFormDirtyState(form.formState.isDirty));
}, [form.formState.isDirty, dispatch]);
const onSubmit = (data: NetworkOnlyFormSchema) => {
const requiresBaseUrl = isCustomProvider;
if (requiresBaseUrl && (data.network_config?.base_url ?? "").trim() === "") {
if ((provider.network_config?.base_url ?? "").trim() !== "") {
toast.error("You can't remove network configuration for this provider.");
} else {
toast.error("Base URL is required for this provider.");
}
return;
}
// Create updated provider configuration
const updatedProvider = buildProviderUpdatePayload(provider, {
network_config: {
...provider.network_config,
base_url: data.network_config?.base_url || undefined,
extra_headers: data.network_config?.extra_headers || undefined,
default_request_timeout_in_seconds: data.network_config?.default_request_timeout_in_seconds ?? 30,
max_retries: data.network_config?.max_retries ?? 0,
retry_backoff_initial: data.network_config?.retry_backoff_initial ?? 500,
retry_backoff_max: data.network_config?.retry_backoff_max ?? 10000,
insecure_skip_verify: data.network_config?.insecure_skip_verify ?? false,
ca_cert_pem: data.network_config?.ca_cert_pem?.trim() || undefined,
stream_idle_timeout_in_seconds:
data.network_config?.stream_idle_timeout_in_seconds ?? DefaultNetworkConfig.stream_idle_timeout_in_seconds,
max_conns_per_host: data.network_config?.max_conns_per_host ?? DefaultNetworkConfig.max_conns_per_host,
enforce_http2: data.network_config?.enforce_http2 ?? DefaultNetworkConfig.enforce_http2,
},
});
updateProvider(updatedProvider)
.unwrap()
.then(() => {
toast.success("Provider configuration updated successfully");
form.reset(data);
})
.catch((err) => {
toast.error("Failed to update provider configuration", {
description: getErrorMessage(err),
});
});
};
useEffect(() => {
// Reset form with new provider's network_config when provider.name changes
form.reset({
network_config: {
base_url: provider.network_config?.base_url || undefined,
extra_headers: provider.network_config?.extra_headers,
default_request_timeout_in_seconds:
provider.network_config?.default_request_timeout_in_seconds ?? DefaultNetworkConfig.default_request_timeout_in_seconds,
max_retries: provider.network_config?.max_retries ?? DefaultNetworkConfig.max_retries,
retry_backoff_initial: provider.network_config?.retry_backoff_initial ?? DefaultNetworkConfig.retry_backoff_initial,
retry_backoff_max: provider.network_config?.retry_backoff_max ?? DefaultNetworkConfig.retry_backoff_max,
insecure_skip_verify: provider.network_config?.insecure_skip_verify ?? DefaultNetworkConfig.insecure_skip_verify,
ca_cert_pem: provider.network_config?.ca_cert_pem ?? DefaultNetworkConfig.ca_cert_pem,
stream_idle_timeout_in_seconds:
provider.network_config?.stream_idle_timeout_in_seconds ?? DefaultNetworkConfig.stream_idle_timeout_in_seconds,
max_conns_per_host: provider.network_config?.max_conns_per_host ?? DefaultNetworkConfig.max_conns_per_host,
},
});
}, [form, provider.name, provider.network_config]);
const baseURLRequired = isCustomProvider;
const hideBaseURL = provider.name === "vllm" || provider.name === "ollama" || provider.name === "sgl";
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 px-6">
{/* Network Configuration */}
<div className="space-y-4">
<div className="grid grid-cols-1 gap-4">
{!hideBaseURL && (
<FormField
control={form.control}
name="network_config.base_url"
render={({ field }) => (
<FormItem>
<FormLabel>Base URL {baseURLRequired ? "(Required)" : "(Optional)"}</FormLabel>
<FormControl>
<Input
placeholder={isCustomProvider ? "https://api.your-provider.com" : "https://api.example.com"}
{...field}
value={field.value || ""}
disabled={!hasUpdateProviderAccess}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<div className="flex w-full flex-row items-start gap-4">
<FormField
control={form.control}
name="network_config.default_request_timeout_in_seconds"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Timeout (seconds)</FormLabel>
<FormControl>
<Input
placeholder="30"
{...field}
value={field.value === undefined || Number.isNaN(field.value) ? "" : field.value}
disabled={!hasUpdateProviderAccess}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(undefined);
return;
}
const parsed = Number(value);
if (!Number.isNaN(parsed)) {
field.onChange(parsed);
}
form.trigger("network_config");
}}
/>
</FormControl>
<FormDescription>{secondsToHumanReadable(field.value)}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="network_config.stream_idle_timeout_in_seconds"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Stream Idle Timeout (seconds)</FormLabel>
<FormControl>
<Input
placeholder="60"
data-testid="network-config-stream-idle-timeout-input"
{...field}
value={field.value === undefined || Number.isNaN(field.value) ? "" : field.value}
disabled={!hasUpdateProviderAccess}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(undefined);
return;
}
const parsed = Number(value);
if (!Number.isNaN(parsed)) {
field.onChange(parsed);
}
form.trigger("network_config");
}}
/>
</FormControl>
<FormDescription>
{field.value ? secondsToHumanReadable(field.value) : ""} Max time to wait for next chunk before closing a stalled
stream
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="network_config.max_retries"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Max Retries</FormLabel>
<FormControl>
<Input
placeholder="0"
{...field}
value={field.value === undefined || Number.isNaN(field.value) ? "" : field.value}
disabled={!hasUpdateProviderAccess}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(undefined);
return;
}
const parsed = Number(value);
if (!Number.isNaN(parsed)) {
field.onChange(parsed);
}
form.trigger("network_config");
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full flex-row items-start gap-4">
<FormField
control={form.control}
name="network_config.retry_backoff_initial"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Initial Backoff (ms)</FormLabel>
<FormControl>
<Input
placeholder="e.g 500"
{...field}
value={field.value === undefined || Number.isNaN(field.value) ? "" : field.value}
disabled={!hasUpdateProviderAccess}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(undefined);
return;
}
const parsed = Number(value);
if (!Number.isNaN(parsed)) {
field.onChange(parsed);
}
form.trigger("network_config");
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="network_config.retry_backoff_max"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Max Backoff (ms)</FormLabel>
<FormControl>
<Input
placeholder="e.g 10000"
{...field}
value={field.value === undefined || Number.isNaN(field.value) ? "" : field.value}
disabled={!hasUpdateProviderAccess}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(undefined);
return;
}
const parsed = Number(value);
if (!Number.isNaN(parsed)) {
field.onChange(parsed);
}
form.trigger("network_config");
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex w-full flex-row items-start gap-4">
<FormField
control={form.control}
name="network_config.max_conns_per_host"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Max Connections Per Host</FormLabel>
<FormControl>
<Input
data-testid="network-config-max-conns-per-host-input"
placeholder="5000"
{...field}
value={field.value === undefined || Number.isNaN(field.value) ? "" : field.value}
disabled={!hasUpdateProviderAccess}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(undefined);
return;
}
const parsed = Number(value);
if (!Number.isNaN(parsed)) {
field.onChange(parsed);
}
form.trigger("network_config");
}}
/>
</FormControl>
<FormDescription>
Max TCP connections per provider host. For HTTP/2 providers (e.g. Bedrock), each connection supports ~100 concurrent
streams.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="network_config.enforce_http2"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between">
<div className="space-y-0.5">
<FormLabel>Enforce HTTP/2</FormLabel>
<FormDescription>
Force HTTP/2 on provider connections. Relevant for net/http-based providers (e.g. Bedrock) where each HTTP/2
connection supports ~100 concurrent streams.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value ?? false}
onCheckedChange={field.onChange}
disabled={!hasUpdateProviderAccess}
data-testid="network-config-enforce-http2"
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="network_config.extra_headers"
render={({ field }) => (
<FormItem>
<FormControl>
<HeadersTable
value={field.value || {}}
onChange={field.onChange}
keyPlaceholder="Header name"
valuePlaceholder="Header value"
label="Extra Headers"
disabled={!hasUpdateProviderAccess}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-4 rounded-lg border p-4">
<h4 className="text-sm font-medium">TLS / Certificate</h4>
<FormField
control={form.control}
name="network_config.insecure_skip_verify"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel>Skip TLS verification</FormLabel>
<FormDescription>
Disable TLS certificate verification for provider connections. This bypasses server certificate validation and
should be used only as a last resort when a trusted CA chain cannot be configured. Prefer ca_cert_pem for
self-signed or private CA deployments.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value ?? false}
onCheckedChange={field.onChange}
disabled={!hasUpdateProviderAccess}
data-testid="network-config-insecure-skip-verify"
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="network_config.ca_cert_pem"
render={({ field }) => (
<FormItem>
<FormLabel>CA Certificate (PEM) (Optional)</FormLabel>
<FormControl>
<Textarea
placeholder="-----BEGIN CERTIFICATE-----&#10;...&#10;-----END CERTIFICATE-----"
className="font-mono text-xs"
rows={6}
{...field}
value={field.value || ""}
disabled={!hasUpdateProviderAccess}
data-testid="network-config-ca-cert-pem"
/>
</FormControl>
<FormDescription>
PEM-encoded CA certificate to trust for provider endpoint connections (e.g. self-signed or internal CA)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
{/* Form Actions */}
<div className="flex justify-end space-x-2 py-2">
{!hideBaseURL && (
<Button
type="button"
variant="outline"
onClick={() => {
form.reset({
network_config: undefined,
});
onSubmit(form.getValues());
}}
disabled={
!hasUpdateProviderAccess ||
isUpdatingProvider ||
!provider.network_config ||
!provider.network_config.base_url ||
provider.network_config.base_url.trim() === ""
}
>
Remove configuration
</Button>
)}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="submit"
disabled={!form.formState.isDirty || !form.formState.isValid || !hasUpdateProviderAccess}
isLoading={isUpdatingProvider}
>
Save Network Configuration
</Button>
</TooltipTrigger>
{(!form.formState.isDirty || !form.formState.isValid) && (
<TooltipContent>
<p>
{!form.formState.isDirty && !form.formState.isValid
? "No changes made and validation errors present"
: !form.formState.isDirty
? "No changes made"
: "Please fix validation errors"}
</p>
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
</div>
</form>
</Form>
);
}

View File

@@ -0,0 +1,111 @@
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { getErrorMessage, setProviderFormDirtyState, useAppDispatch } from "@/lib/store";
import { useUpdateProviderMutation } from "@/lib/store/apis/providersApi";
import type { ModelProvider } from "@/lib/types/config";
import { openaiConfigFormSchema, type OpenAIConfigFormSchema } from "@/lib/types/schemas";
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm, type Resolver } from "react-hook-form";
import { toast } from "sonner";
import { buildProviderUpdatePayload } from "../views/utils";
interface OpenAIConfigFormFragmentProps {
provider: ModelProvider;
}
export function OpenAIConfigFormFragment({ provider }: OpenAIConfigFormFragmentProps) {
const dispatch = useAppDispatch();
const hasUpdateProviderAccess = useRbac(RbacResource.ModelProvider, RbacOperation.Update);
const [updateProvider, { isLoading: isUpdatingProvider }] = useUpdateProviderMutation();
const form = useForm<OpenAIConfigFormSchema, any, OpenAIConfigFormSchema>({
resolver: zodResolver(openaiConfigFormSchema) as Resolver<OpenAIConfigFormSchema, any, OpenAIConfigFormSchema>,
mode: "onChange",
reValidateMode: "onChange",
defaultValues: {
disable_store: provider.openai_config?.disable_store ?? false,
},
});
useEffect(() => {
dispatch(setProviderFormDirtyState(form.formState.isDirty));
}, [form.formState.isDirty, dispatch]);
useEffect(() => {
form.reset({
disable_store: provider.openai_config?.disable_store ?? false,
});
}, [form, provider.name, provider.openai_config?.disable_store]);
const onSubmit = (data: OpenAIConfigFormSchema) => {
updateProvider(
buildProviderUpdatePayload(provider, {
openai_config: {
disable_store: data.disable_store,
},
}),
)
.unwrap()
.then(() => {
toast.success("OpenAI configuration updated successfully");
form.reset(data);
})
.catch((err) => {
toast.error("Failed to update OpenAI configuration", {
description: getErrorMessage(err),
});
});
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 px-6" data-testid="provider-config-openai-content">
<div className="space-y-4">
<FormField
control={form.control}
name="disable_store"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between space-x-2">
<div className="space-y-0.5">
<FormLabel>Disable Store</FormLabel>
<p className="text-muted-foreground text-xs">
With the Responses API, store defaults to true, and when it is on, the generated response is stored for later
retrieval via API. OpenAI exposes endpoints to retrieve and delete stored responses, so your response IDs become
durable server-side objects instead of one-shot IDs.
</p>
</div>
<FormControl>
<Switch
data-testid="provider-openai-disable-store-switch"
size="md"
checked={field.value}
disabled={!hasUpdateProviderAccess}
onCheckedChange={(checked) => {
field.onChange(checked);
form.trigger("disable_store");
}}
/>
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end space-x-2 pb-6">
<Button
type="submit"
disabled={!form.formState.isDirty || !form.formState.isValid || !hasUpdateProviderAccess || isUpdatingProvider}
isLoading={isUpdatingProvider}
>
Save OpenAI Configuration
</Button>
</div>
</form>
</Form>
);
}

View File

@@ -0,0 +1,159 @@
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { DefaultPerformanceConfig } from "@/lib/constants/config";
import { getErrorMessage, setProviderFormDirtyState, useAppDispatch } from "@/lib/store";
import { useUpdateProviderMutation } from "@/lib/store/apis/providersApi";
import { ModelProvider } from "@/lib/types/config";
import { performanceFormSchema, type PerformanceFormSchema } from "@/lib/types/schemas";
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm, type Resolver } from "react-hook-form";
import { toast } from "sonner";
import { buildProviderUpdatePayload } from "../views/utils";
interface PerformanceFormFragmentProps {
provider: ModelProvider;
}
export function PerformanceFormFragment({ provider }: PerformanceFormFragmentProps) {
const dispatch = useAppDispatch();
const hasUpdateProviderAccess = useRbac(RbacResource.ModelProvider, RbacOperation.Update);
const [updateProvider, { isLoading: isUpdatingProvider }] = useUpdateProviderMutation();
const form = useForm<PerformanceFormSchema, any, PerformanceFormSchema>({
resolver: zodResolver(performanceFormSchema) as Resolver<PerformanceFormSchema, any, PerformanceFormSchema>,
mode: "onChange",
reValidateMode: "onChange",
defaultValues: {
concurrency_and_buffer_size: {
concurrency: provider.concurrency_and_buffer_size?.concurrency ?? DefaultPerformanceConfig.concurrency,
buffer_size: provider.concurrency_and_buffer_size?.buffer_size ?? DefaultPerformanceConfig.buffer_size,
},
},
});
useEffect(() => {
dispatch(setProviderFormDirtyState(form.formState.isDirty));
}, [form.formState.isDirty]);
useEffect(() => {
// Reset form with new provider's concurrency_and_buffer_size when provider changes
form.reset({
concurrency_and_buffer_size: {
concurrency: provider.concurrency_and_buffer_size?.concurrency ?? DefaultPerformanceConfig.concurrency,
buffer_size: provider.concurrency_and_buffer_size?.buffer_size ?? DefaultPerformanceConfig.buffer_size,
},
});
}, [form, provider.name, provider.concurrency_and_buffer_size]);
const onSubmit = (data: PerformanceFormSchema) => {
// Create updated provider configuration (raw request/response are in Debugging tab)
const updatedProvider = buildProviderUpdatePayload(provider, {
concurrency_and_buffer_size: {
concurrency: data.concurrency_and_buffer_size.concurrency,
buffer_size: data.concurrency_and_buffer_size.buffer_size,
},
});
updateProvider(updatedProvider)
.unwrap()
.then(() => {
toast.success("Provider configuration updated successfully");
form.reset(data);
})
.catch((err) => {
toast.error("Failed to update provider configuration", {
description: getErrorMessage(err),
});
});
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 px-6">
{/* Performance Configuration */}
<div className="space-y-4">
<div className="flex flex-row gap-4">
<div className="flex-1">
<FormField
control={form.control}
name="concurrency_and_buffer_size.concurrency"
render={({ field }) => (
<FormItem>
<FormLabel>Concurrency</FormLabel>
<FormControl>
<Input
type="number"
placeholder="10"
{...field}
value={field.value === undefined || Number.isNaN(field.value) ? "" : field.value}
disabled={!hasUpdateProviderAccess}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(undefined);
return;
}
const parsed = Number.parseInt(value);
if (!Number.isNaN(parsed)) {
field.onChange(parsed);
}
form.trigger("concurrency_and_buffer_size");
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex-1">
<FormField
control={form.control}
name="concurrency_and_buffer_size.buffer_size"
render={({ field }) => (
<FormItem>
<FormLabel>Buffer Size</FormLabel>
<FormControl>
<Input
type="number"
placeholder="10"
{...field}
value={field.value === undefined || Number.isNaN(field.value) ? "" : field.value}
disabled={!hasUpdateProviderAccess}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(undefined);
return;
}
const parsed = Number.parseInt(value);
if (!Number.isNaN(parsed)) {
field.onChange(parsed);
}
form.trigger("concurrency_and_buffer_size");
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
{/* Form Actions */}
<div className="flex justify-end space-x-2 pb-6">
<Button
type="submit"
disabled={!form.formState.isDirty || !form.formState.isValid || !hasUpdateProviderAccess || isUpdatingProvider}
isLoading={isUpdatingProvider}
>
Save Performance Configuration
</Button>
</div>
</form>
</Form>
);
}

View File

@@ -0,0 +1,226 @@
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { getErrorMessage, setProviderFormDirtyState, useAppDispatch } from "@/lib/store";
import { useUpdateProviderMutation } from "@/lib/store/apis/providersApi";
import { ModelProvider } from "@/lib/types/config";
import { proxyOnlyFormSchema, type ProxyOnlyFormSchema } from "@/lib/types/schemas";
import { cn } from "@/lib/utils";
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { buildProviderUpdatePayload } from "../views/utils";
interface ProxyFormFragmentProps {
provider: ModelProvider;
}
export function ProxyFormFragment({ provider }: ProxyFormFragmentProps) {
const dispatch = useAppDispatch();
const hasUpdateProviderAccess = useRbac(RbacResource.ModelProvider, RbacOperation.Update);
const [updateProvider, { isLoading: isUpdatingProvider }] = useUpdateProviderMutation();
const form = useForm<ProxyOnlyFormSchema>({
resolver: zodResolver(proxyOnlyFormSchema),
mode: "onChange",
reValidateMode: "onChange",
defaultValues: {
proxy_config: {
type: provider.proxy_config?.type,
url: provider.proxy_config?.url || "",
username: provider.proxy_config?.username || "",
password: provider.proxy_config?.password || "",
ca_cert_pem: provider.proxy_config?.ca_cert_pem || "",
},
},
});
useEffect(() => {
dispatch(setProviderFormDirtyState(form.formState.isDirty));
}, [form.formState.isDirty]);
useEffect(() => {
form.reset({
proxy_config: {
type: provider.proxy_config?.type,
url: provider.proxy_config?.url || "",
username: provider.proxy_config?.username || "",
password: provider.proxy_config?.password || "",
ca_cert_pem: provider.proxy_config?.ca_cert_pem || "",
},
});
}, [form, provider.name, provider.proxy_config]);
const watchedProxyType = form.watch("proxy_config.type");
const onSubmit = (data: ProxyOnlyFormSchema) => {
updateProvider(
buildProviderUpdatePayload(provider, {
proxy_config: {
type: data.proxy_config?.type ?? "none",
url: data.proxy_config?.url || undefined,
username: data.proxy_config?.username || undefined,
password: data.proxy_config?.password || undefined,
ca_cert_pem: data.proxy_config?.ca_cert_pem || undefined,
},
}),
)
.unwrap()
.then(() => {
toast.success("Provider configuration updated successfully");
form.reset(data);
})
.catch((err) => {
toast.error("Failed to update provider configuration", {
description: getErrorMessage(err),
});
});
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 px-6">
{/* Proxy Configuration */}
<div className="space-y-4">
<div className="space-y-4">
<FormField
control={form.control}
name="proxy_config.type"
render={({ field }) => (
<FormItem>
<FormLabel>Proxy Type</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value === "none" ? "" : field.value}
disabled={!hasUpdateProviderAccess}
>
<FormControl>
<SelectTrigger className="w-48">
<SelectValue placeholder="Select type" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="http">HTTP</SelectItem>
<SelectItem value="socks5">SOCKS5</SelectItem>
<SelectItem value="environment">Environment</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<div
className={cn(
"block transition-all duration-200",
(!watchedProxyType || watchedProxyType === "none" || watchedProxyType === "environment") && "hidden",
)}
>
<div className="space-y-4 pt-2">
<FormField
control={form.control}
name="proxy_config.url"
render={({ field }) => (
<FormItem>
<FormLabel>Proxy URL</FormLabel>
<FormControl>
<Input
placeholder="http://proxy.example.com"
{...field}
value={field.value || ""}
disabled={!hasUpdateProviderAccess}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="proxy_config.username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Proxy username" {...field} value={field.value || ""} disabled={!hasUpdateProviderAccess} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="proxy_config.password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Proxy password"
{...field}
value={field.value || ""}
disabled={!hasUpdateProviderAccess}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="proxy_config.ca_cert_pem"
render={({ field }) => (
<FormItem>
<FormLabel>CA Certificate (PEM) (Optional)</FormLabel>
<FormControl>
<Textarea
placeholder="-----BEGIN CERTIFICATE-----&#10;...&#10;-----END CERTIFICATE-----"
className="font-mono text-xs"
rows={6}
{...field}
value={field.value || ""}
disabled={!hasUpdateProviderAccess}
/>
</FormControl>
<FormDescription>
PEM-encoded CA certificate to trust for TLS connections through SSL-intercepting proxies
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
</div>
</div>
{/* Form Actions */}
<div className="flex justify-end space-x-2 pb-6">
<Button
type="button"
variant="outline"
onClick={() => {
onSubmit({ proxy_config: { type: "none", url: "" } });
}}
disabled={!hasUpdateProviderAccess || isUpdatingProvider || !provider.proxy_config || provider.proxy_config.type === "none"}
>
Remove configuration
</Button>
<Button
type="submit"
disabled={!form.formState.isDirty || !form.formState.isValid || !hasUpdateProviderAccess || isUpdatingProvider}
isLoading={isUpdatingProvider}
>
Save Proxy Configuration
</Button>
</div>
</form>
</Form>
);
}