first commit
This commit is contained in:
244
ui/app/workspace/providers/dialogs/addNewCustomProviderSheet.tsx
Normal file
244
ui/app/workspace/providers/dialogs/addNewCustomProviderSheet.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, 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 { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { getErrorMessage, useCreateProviderMutation } from "@/lib/store";
|
||||
import { BaseProvider, ModelProviderName } from "@/lib/types/config";
|
||||
import { allowedRequestsSchema } from "@/lib/types/schemas";
|
||||
import { cleanPathOverrides } from "@/lib/utils/validation";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { AllowedRequestsFields } from "../fragments/allowedRequestsFields";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
baseFormat: z.string().min(1),
|
||||
base_url: z.string().min(1, "Base URL is required").url("Must be a valid URL"),
|
||||
allowed_requests: allowedRequestsSchema,
|
||||
request_path_overrides: z.record(z.string(), z.string().optional()).optional(),
|
||||
is_key_less: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export interface AddCustomProviderSheetContentProps {
|
||||
show?: boolean;
|
||||
onSave: (id: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface Props extends AddCustomProviderSheetContentProps {
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
export function AddCustomProviderSheetContent({ show = true, onClose, onSave }: AddCustomProviderSheetContentProps) {
|
||||
const [addProvider, { isLoading: isAddingProvider }] = useCreateProviderMutation();
|
||||
const form = useForm<FormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
baseFormat: "",
|
||||
base_url: "",
|
||||
allowed_requests: {
|
||||
text_completion: true,
|
||||
text_completion_stream: true,
|
||||
chat_completion: true,
|
||||
chat_completion_stream: true,
|
||||
responses: true,
|
||||
responses_stream: true,
|
||||
embedding: true,
|
||||
speech: true,
|
||||
speech_stream: true,
|
||||
transcription: true,
|
||||
transcription_stream: true,
|
||||
image_generation: true,
|
||||
image_generation_stream: true,
|
||||
image_edit: true,
|
||||
image_edit_stream: true,
|
||||
image_variation: true,
|
||||
rerank: true,
|
||||
ocr: true,
|
||||
ocr_stream: true,
|
||||
video_generation: true,
|
||||
video_retrieve: true,
|
||||
video_download: true,
|
||||
video_delete: true,
|
||||
video_list: true,
|
||||
video_remix: true,
|
||||
count_tokens: true,
|
||||
list_models: true,
|
||||
websocket_responses: true,
|
||||
realtime: false,
|
||||
},
|
||||
request_path_overrides: undefined,
|
||||
is_key_less: false,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
form.clearErrors();
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
const onSubmit = (data: FormData) => {
|
||||
const payload = {
|
||||
provider: data.name as ModelProviderName,
|
||||
custom_provider_config: {
|
||||
base_provider_type: data.baseFormat as BaseProvider,
|
||||
allowed_requests: data.allowed_requests,
|
||||
request_path_overrides: cleanPathOverrides(data.request_path_overrides),
|
||||
is_key_less: data.is_key_less ?? false,
|
||||
},
|
||||
network_config: {
|
||||
base_url: data.base_url,
|
||||
default_request_timeout_in_seconds: 30,
|
||||
max_retries: 0,
|
||||
retry_backoff_initial: 500,
|
||||
retry_backoff_max: 5000,
|
||||
},
|
||||
};
|
||||
|
||||
addProvider(payload)
|
||||
.unwrap()
|
||||
.then((provider) => {
|
||||
onSave(provider.name);
|
||||
form.reset();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error("Failed to add provider", {
|
||||
description: getErrorMessage(err),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const baseFormat = form.watch("baseFormat") as BaseProvider;
|
||||
const isKeyLessDisabled = baseFormat === "bedrock";
|
||||
|
||||
return (
|
||||
<>
|
||||
<SheetHeader className="flex shrink-0 flex-col items-start">
|
||||
<SheetTitle>Add Custom Provider</SheetTitle>
|
||||
<SheetDescription>Enter the details of your custom provider.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||
<div className="custom-scrollbar min-h-0 flex-1 space-y-4 overflow-y-auto">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col gap-3">
|
||||
<FormLabel className="text-right">Name</FormLabel>
|
||||
<div className="col-span-3">
|
||||
<FormControl>
|
||||
<Input placeholder="Name" data-testid="custom-provider-name" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="baseFormat"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col gap-3">
|
||||
<FormLabel>Base Format</FormLabel>
|
||||
<div>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full" data-testid="base-provider-select">
|
||||
<SelectValue placeholder="Select base format" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="openai">OpenAI</SelectItem>
|
||||
<SelectItem value="anthropic">Anthropic</SelectItem>
|
||||
<SelectItem value="gemini">Gemini</SelectItem>
|
||||
<SelectItem value="cohere">Cohere</SelectItem>
|
||||
<SelectItem value="bedrock">AWS Bedrock</SelectItem>
|
||||
<SelectItem value="replicate">Replicate</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="base_url"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col gap-3">
|
||||
<FormLabel>Base URL</FormLabel>
|
||||
<div>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"https://api.your-provider.com"}
|
||||
data-testid="base-url-input"
|
||||
{...field}
|
||||
value={field.value || ""}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</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}
|
||||
data-testid="custom-provider-keyless-switch"
|
||||
/>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{/* Allowed Requests Configuration */}
|
||||
<AllowedRequestsFields control={form.control} providerType={form.watch("baseFormat") as BaseProvider} />
|
||||
<div className="align-end mt-10 ml-auto flex flex-row gap-2 border-t pt-4">
|
||||
<Button type="button" variant="outline" onClick={onClose} className="ml-auto" data-testid="custom-provider-cancel-btn">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" isLoading={isAddingProvider} data-testid="custom-provider-save-btn">
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AddCustomProviderSheet(props: Props) {
|
||||
return (
|
||||
<Sheet open={props.show} onOpenChange={(open) => !open && props.onClose()}>
|
||||
<SheetContent className="custom-scrollbar flex flex-col p-8 sm:max-w-3xl" data-testid="custom-provider-sheet">
|
||||
<AddCustomProviderSheetContent {...props} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
57
ui/app/workspace/providers/dialogs/addNewKeySheet.tsx
Normal file
57
ui/app/workspace/providers/dialogs/addNewKeySheet.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import Provider from "@/components/provider";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
||||
import { ModelProvider } from "@/lib/types/config";
|
||||
import { toast } from "sonner";
|
||||
import ProviderKeyForm from "../views/providerKeyForm";
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
onCancel: () => void;
|
||||
provider: ModelProvider;
|
||||
keyId: string | null;
|
||||
providerName?: string;
|
||||
}
|
||||
|
||||
export default function AddNewKeySheet({ show, onCancel, provider, keyId, providerName }: Props) {
|
||||
const isEditing = keyId !== null;
|
||||
const resolvedProviderName = (providerName ?? provider.name).toLowerCase();
|
||||
const isVLLM = resolvedProviderName === "vllm";
|
||||
const isOllamaOrSGL = resolvedProviderName === "ollama" || resolvedProviderName === "sgl";
|
||||
const entityLabel = isVLLM ? "model" : isOllamaOrSGL ? "server" : "key";
|
||||
const EntityLabel = entityLabel.charAt(0).toUpperCase() + entityLabel.slice(1);
|
||||
const dialogTitle = isEditing ? `Edit ${entityLabel}` : `Add new ${entityLabel}`;
|
||||
const successMessage = isEditing ? `${EntityLabel} updated successfully` : `${EntityLabel} added successfully`;
|
||||
|
||||
return (
|
||||
<Sheet
|
||||
open={show}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onCancel();
|
||||
}}
|
||||
>
|
||||
<SheetContent className="custom-scrollbar p-8" data-testid="key-form" onInteractOutside={(e) => e.preventDefault()}>
|
||||
<SheetHeader className="flex flex-col items-start">
|
||||
<SheetTitle>
|
||||
<div className="font-lg flex items-center gap-2">
|
||||
<div className={"flex items-center"}>
|
||||
<Provider provider={provider.name} size={24} />:
|
||||
</div>
|
||||
{dialogTitle}
|
||||
</div>
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div>
|
||||
<ProviderKeyForm
|
||||
provider={provider}
|
||||
keyId={keyId}
|
||||
onCancel={onCancel}
|
||||
onSave={() => {
|
||||
toast.success(successMessage);
|
||||
onCancel();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
} from "@/components/ui/alertDialog";
|
||||
import { getErrorMessage, useDeleteProviderMutation } from "@/lib/store";
|
||||
import { ModelProvider } from "@/lib/types/config";
|
||||
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
|
||||
import { AlertDialogTitle } from "@radix-ui/react-alert-dialog";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
onCancel: () => void;
|
||||
onDelete: () => void;
|
||||
provider: ModelProvider;
|
||||
}
|
||||
|
||||
export default function ConfirmDeleteProviderDialog({ show, onCancel, onDelete, provider }: Props) {
|
||||
const [deleteProvider, { isLoading: isDeletingProvider }] = useDeleteProviderMutation();
|
||||
const hasDeleteAccess = useRbac(RbacResource.ModelProvider, RbacOperation.Delete);
|
||||
|
||||
const onDeleteHandler = () => {
|
||||
deleteProvider(provider.name)
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
onDelete();
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error("Failed to delete provider", {
|
||||
description: getErrorMessage(err),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertDialog open={show}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Provider</AlertDialogTitle>
|
||||
<AlertDialogDescription>Are you sure you want to delete this provider? This action cannot be undone.</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onDeleteHandler} disabled={isDeletingProvider || !hasDeleteAccess}>
|
||||
{isDeletingProvider ? "Deleting..." : "Delete"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
39
ui/app/workspace/providers/dialogs/confirmRedirection.tsx
Normal file
39
ui/app/workspace/providers/dialogs/confirmRedirection.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alertDialog";
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
onContinue: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export default function ConfirmRedirectionDialog({ show, onContinue, onCancel }: Props) {
|
||||
return (
|
||||
<AlertDialog open={show}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirm Redirection</AlertDialogTitle>
|
||||
<AlertDialogDescription>You have unsaved data. Are you sure you want to continue?</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter className="mt-4">
|
||||
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
onContinue();
|
||||
}}
|
||||
>
|
||||
Continue
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
155
ui/app/workspace/providers/dialogs/providerConfigSheet.tsx
Normal file
155
ui/app/workspace/providers/dialogs/providerConfigSheet.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import Provider from "@/components/provider";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ModelProvider } from "@/lib/types/config";
|
||||
import { RbacOperation, RbacResource, useRbac } from "@enterprise/lib";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
ApiStructureFormFragment,
|
||||
BetaHeadersFormFragment,
|
||||
GovernanceFormFragment,
|
||||
OpenAIConfigFormFragment,
|
||||
ProxyFormFragment,
|
||||
} from "../fragments";
|
||||
import { DebuggingFormFragment } from "../fragments/debuggingFormFragment";
|
||||
import { NetworkFormFragment } from "../fragments/networkFormFragment";
|
||||
import { PerformanceFormFragment } from "../fragments/performanceFormFragment";
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
onCancel: () => void;
|
||||
provider: ModelProvider;
|
||||
}
|
||||
|
||||
const ANTHROPIC_FAMILY_PROVIDERS = ["anthropic", "vertex", "bedrock", "azure"];
|
||||
|
||||
const availableTabs = (hasCustomProviderConfig: boolean, hasGovernanceAccess: boolean, isOpenAI: boolean, isAnthropicFamily: boolean) => {
|
||||
const tabs = [];
|
||||
if (hasCustomProviderConfig) {
|
||||
tabs.push({
|
||||
id: "api-structure",
|
||||
label: "API Structure",
|
||||
});
|
||||
}
|
||||
tabs.push({
|
||||
id: "network",
|
||||
label: "Network",
|
||||
});
|
||||
tabs.push({
|
||||
id: "proxy",
|
||||
label: "Proxy",
|
||||
});
|
||||
tabs.push({
|
||||
id: "performance",
|
||||
label: "Performance",
|
||||
});
|
||||
if (hasGovernanceAccess) {
|
||||
tabs.push({
|
||||
id: "governance",
|
||||
label: "Governance",
|
||||
});
|
||||
}
|
||||
if (isAnthropicFamily) {
|
||||
tabs.push({
|
||||
id: "beta-headers",
|
||||
label: "Beta Headers",
|
||||
});
|
||||
}
|
||||
tabs.push({
|
||||
id: "debugging",
|
||||
label: "Debugging",
|
||||
});
|
||||
if (isOpenAI) {
|
||||
tabs.push({
|
||||
id: "openai-config",
|
||||
label: "OpenAI Config",
|
||||
});
|
||||
}
|
||||
return tabs;
|
||||
};
|
||||
|
||||
export default function ProviderConfigSheet({ show, onCancel, provider }: Props) {
|
||||
const [selectedTab, setSelectedTab] = useState<string | undefined>(undefined);
|
||||
const hasGovernanceAccess = useRbac(RbacResource.Governance, RbacOperation.View);
|
||||
const hasCustomProviderConfig = !!provider.custom_provider_config;
|
||||
const isOpenAI = provider.name === "openai";
|
||||
const isAnthropicFamily = ANTHROPIC_FAMILY_PROVIDERS.includes(provider.name.toLowerCase());
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
return availableTabs(hasCustomProviderConfig, hasGovernanceAccess, isOpenAI, isAnthropicFamily);
|
||||
}, [hasCustomProviderConfig, hasGovernanceAccess, isOpenAI, isAnthropicFamily]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTab((previousTab) => {
|
||||
if (previousTab && tabs.some((tab) => tab.id === previousTab)) {
|
||||
return previousTab;
|
||||
}
|
||||
|
||||
return tabs[0]?.id;
|
||||
});
|
||||
}, [tabs]);
|
||||
|
||||
return (
|
||||
<Sheet
|
||||
open={show}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) onCancel();
|
||||
}}
|
||||
>
|
||||
<SheetContent className="custom-scrollbar p-8 sm:max-w-[50%]">
|
||||
<SheetHeader className="flex flex-col items-start">
|
||||
<SheetTitle>
|
||||
<div className="font-lg flex items-center gap-2">
|
||||
<div className="flex items-center">
|
||||
<Provider provider={provider.name} size={24} />
|
||||
</div>
|
||||
Provider configuration
|
||||
</div>
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="w-full rounded-sm border">
|
||||
<Tabs defaultValue={tabs[0]?.id} value={selectedTab} onValueChange={setSelectedTab} className="space-y-6">
|
||||
<div className="custom-scrollbar mb-4 w-full overflow-x-auto">
|
||||
<TabsList className="h-10 w-max min-w-full justify-start rounded-tl-sm rounded-tr-sm rounded-br-none rounded-bl-none">
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
data-testid={`provider-tab-${tab.id}`}
|
||||
className="flex-none px-3 whitespace-nowrap"
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</div>
|
||||
<TabsContent value="api-structure">
|
||||
<ApiStructureFormFragment provider={provider} />
|
||||
</TabsContent>
|
||||
<TabsContent value="openai-config">
|
||||
<OpenAIConfigFormFragment provider={provider} />
|
||||
</TabsContent>
|
||||
<TabsContent value="network">
|
||||
<NetworkFormFragment provider={provider} />
|
||||
</TabsContent>
|
||||
<TabsContent value="proxy">
|
||||
<ProxyFormFragment provider={provider} />
|
||||
</TabsContent>
|
||||
<TabsContent value="performance">
|
||||
<PerformanceFormFragment provider={provider} />
|
||||
</TabsContent>
|
||||
<TabsContent value="governance">
|
||||
<GovernanceFormFragment provider={provider} />
|
||||
</TabsContent>
|
||||
<TabsContent value="beta-headers">
|
||||
<BetaHeadersFormFragment provider={provider} />
|
||||
</TabsContent>
|
||||
<TabsContent value="debugging">
|
||||
<DebuggingFormFragment provider={provider} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user