Skip to content

Commit

Permalink
feat: add default model alias crud to admin ui (#676)
Browse files Browse the repository at this point in the history
* feat: add default model alias crud to admin ui

* chore: remove the `default` property from the model object
  • Loading branch information
ryanhopperlowe authored Nov 26, 2024
1 parent e201726 commit fd56256
Show file tree
Hide file tree
Showing 10 changed files with 353 additions and 50 deletions.
16 changes: 9 additions & 7 deletions ui/admin/app/components/form/BasicInputItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@ import {
FormMessage,
} from "~/components/ui/form";

export function BasicInputItem({
children,
classNames = {},
label,
description,
}: {
export type BasicInputItemProps = {
children: ReactNode;
classNames?: {
wrapper?: string;
Expand All @@ -23,7 +18,14 @@ export function BasicInputItem({
};
label?: ReactNode;
description?: ReactNode;
}) {
};

export function BasicInputItem({
children,
classNames = {},
label,
description,
}: BasicInputItemProps) {
return (
<FormItem className={classNames.wrapper}>
{label && (
Expand Down
13 changes: 11 additions & 2 deletions ui/admin/app/components/form/controlledInputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {

import { cn } from "~/lib/utils";

import { BasicInputItem } from "~/components/form/BasicInputItem";
import {
BasicInputItem,
BasicInputItemProps,
} from "~/components/form/BasicInputItem";
import { Checkbox } from "~/components/ui/checkbox";
import {
FormControl,
Expand Down Expand Up @@ -232,6 +235,7 @@ export type ControlledCustomInputProps<
TValues extends FieldValues,
TName extends FieldPath<TValues>,
> = BaseProps<TValues, TName> & {
classNames?: BasicInputItemProps["classNames"];
children: (props: {
field: ControllerRenderProps<TValues, TName>;
fieldState: ControllerFieldState;
Expand All @@ -248,14 +252,19 @@ export function ControlledCustomInput<
name,
label,
description,
classNames,
children,
}: ControlledCustomInputProps<TValues, TName>) {
return (
<FormField
control={control}
name={name}
render={(args) => (
<BasicInputItem label={label} description={description}>
<BasicInputItem
classNames={classNames}
label={label}
description={description}
>
{children({
...args,
className: getFieldStateClasses(args.fieldState),
Expand Down
222 changes: 222 additions & 0 deletions ui/admin/app/components/model/DefaultModelAliasForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import useSWR from "swr";

import { UpdateDefaultModelAlias } from "~/lib/model/defaultModelAliases";
import { Model, getModelUsageFromAlias } from "~/lib/model/models";
import { DefaultModelAliasApiService } from "~/lib/service/api/defaultModelAliasApiService";
import { ModelApiService } from "~/lib/service/api/modelApiService";

import { Button } from "~/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "~/components/ui/form";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { useAsync } from "~/hooks/useAsync";

export function DefaultModelAliasForm({
onSuccess,
}: {
onSuccess?: () => void;
}) {
const { data: defaultAliases } = useSWR(
DefaultModelAliasApiService.getAliases.key(),
DefaultModelAliasApiService.getAliases
);

const { data: models } = useSWR(
ModelApiService.getModels.key(),
ModelApiService.getModels
);

const modelUsageMap = useMemo(() => {
return (models ?? []).reduce((acc, model) => {
if (!acc.has(model.usage)) acc.set(model.usage, []);

acc.get(model.usage)?.push(model);

return acc;
}, new Map<string, Model[]>());
}, [models]);

const update = useAsync(
async (updates: UpdateDefaultModelAlias[]) => {
await Promise.all(
updates.map((update) =>
DefaultModelAliasApiService.updateAlias(
update.alias,
update
)
)
);
},
{
onSuccess: () => {
toast.success("Default model aliases updated");
onSuccess?.();
},
}
);

const defaultValues = useMemo(() => {
return defaultAliases?.reduce(
(acc, alias) => {
acc[alias.alias] = alias.model;
return acc;
},
{} as Record<string, string>
);
}, [defaultAliases]);

const form = useForm<Record<string, string>>({ defaultValues });

useEffect(() => {
return form.watch((values) => {
const changedItems = defaultAliases?.filter(({ alias, model }) => {
return values[alias] !== model;
});

if (!changedItems?.length) return;
}).unsubscribe;
}, [defaultAliases, form]);

useEffect(() => {
form.reset(defaultValues);
}, [defaultValues, form]);

const handleSubmit = form.handleSubmit((values) => {
const updates = defaultAliases
?.filter(({ alias, model }) => values[alias] !== model)
.map(({ alias }) => ({
alias,
model: values[alias],
}));

update.execute(updates ?? []);
});

return (
<Form {...form}>
<form onSubmit={handleSubmit} className="space-y-6">
{defaultAliases?.map(({ alias, model: defaultModel }) => (
<FormField
control={form.control}
name={alias}
key={alias}
render={({ field: { ref: _, ...field } }) => {
const usage = getModelUsageFromAlias(alias);
const modelOptions = usage
? modelUsageMap.get(usage)
: [];

return (
<FormItem className="flex justify-between items-center space-y-0">
<FormLabel>{alias}</FormLabel>

<div className="flex flex-col gap-2 w-[50%]">
<FormControl>
<Select
{...field}
key={field.value}
value={field.value || ""}
onValueChange={field.onChange}
>
<SelectTrigger className="w-full">
<SelectValue
placeholder={
defaultModel
}
/>
</SelectTrigger>

<SelectContent>
{modelOptions ? (
modelOptions.map(
(model) => (
<SelectItem
key={
model.id
}
value={
model.id
}
>
{model.id}
</SelectItem>
)
)
) : (
<SelectItem
value={defaultModel}
>
{defaultModel}
</SelectItem>
)}
</SelectContent>
</Select>
</FormControl>

<FormMessage />
</div>
</FormItem>
);
}}
/>
))}

<Button
type="submit"
className="w-full"
disabled={update.isLoading}
loading={update.isLoading}
>
Save Changes
</Button>
</form>
</Form>
);
}

export function DefaultModelAliasFormDialog() {
const [open, setOpen] = useState(false);

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Default Model Aliases</Button>
</DialogTrigger>

<DialogContent>
<DialogHeader>
<DialogTitle>Default Model Aliases</DialogTitle>
</DialogHeader>

<DialogDescription>
Set the default model for each usage.
</DialogDescription>

<DefaultModelAliasForm onSuccess={() => setOpen(false)} />
</DialogContent>
</Dialog>
);
}
17 changes: 3 additions & 14 deletions ui/admin/app/components/model/ModelForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,9 @@ import {
getModelUsageLabel,
getModelsForProvider,
} from "~/lib/model/models";
import { BadRequestError } from "~/lib/service/api/apiErrors";
import { ModelApiService } from "~/lib/service/api/modelApiService";

import {
ControlledCheckbox,
ControlledCustomInput,
} from "~/components/form/controlledInputs";
import { ControlledCustomInput } from "~/components/form/controlledInputs";
import { Button } from "~/components/ui/button";
import { Form } from "~/components/ui/form";
import {
Expand Down Expand Up @@ -71,7 +67,6 @@ export function ModelForm(props: ModelFormProps) {
targetModel: model?.targetModel ?? "",
modelProvider: model?.modelProvider ?? "",
active: model?.active ?? true,
default: model?.default ?? false,
usage: model?.usage ?? ModelUsage.LLM,
};
}, [model]);
Expand Down Expand Up @@ -184,12 +179,6 @@ export function ModelForm(props: ModelFormProps) {
)}
</ControlledCustomInput>

<ControlledCheckbox
control={form.control}
name="default"
label="Default Model"
/>

<Button
type="submit"
className="w-full"
Expand Down Expand Up @@ -220,7 +209,7 @@ export function ModelForm(props: ModelFormProps) {
}

function onError(error: unknown) {
if (error instanceof BadRequestError)
form.setError("default", { message: error.message });
if (error instanceof Error) toast.error(error.message);
else toast.error("Model failed to save.");
}
}
9 changes: 9 additions & 0 deletions ui/admin/app/lib/model/defaultModelAliases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type DefaultModelAliasBase = {
alias: string;
model: string;
};

export type DefaultModelAlias = DefaultModelAliasBase;

export type CreateDefaultModelAlias = DefaultModelAliasBase;
export type UpdateDefaultModelAlias = DefaultModelAliasBase;
15 changes: 13 additions & 2 deletions ui/admin/app/lib/model/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export type ModelManifest = {
targetModel?: string;
modelProvider: string;
active: boolean;
default: boolean;
usage: ModelUsage;
};

Expand All @@ -42,7 +41,6 @@ export const ModelManifestSchema = z.object({
targetModel: z.string().min(1, "Required"),
modelProvider: z.string().min(1, "Required"),
active: z.boolean(),
default: z.boolean(),
usage: z.nativeEnum(ModelUsage),
});

Expand Down Expand Up @@ -91,6 +89,19 @@ const ModelToProviderMap = {
],
};

export const ModelAliasToUsageMap = {
llm: ModelUsage.LLM,
"llm-mini": ModelUsage.LLM,
"text-embedding": ModelUsage.TextEmbedding,
"image-generation": ModelUsage.ImageGeneration,
} as const;

export function getModelUsageFromAlias(alias: string) {
if (!(alias in ModelAliasToUsageMap)) return null;

return ModelAliasToUsageMap[alias as keyof typeof ModelAliasToUsageMap];
}

export function getModelsForProvider(providerId: string) {
if (!providerId || !(providerId in ModelToProviderMap)) return [];
return ModelToProviderMap[providerId as keyof typeof ModelToProviderMap];
Expand Down
Loading

0 comments on commit fd56256

Please sign in to comment.