first commit
This commit is contained in:
522
ui/app/workspace/providers/fragments/networkFormFragment.tsx
Normal file
522
ui/app/workspace/providers/fragments/networkFormFragment.tsx
Normal 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----- ... -----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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user