From 8e4f2cc5f879cf18becc80bce33db12d1fe86bca Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 3 Apr 2026 15:54:21 -0600 Subject: [PATCH] Add frontend --- frigate/api/app.py | 10 ++ .../config-form/section-configs/genai.ts | 1 + .../config-form/theme/frigateTheme.ts | 2 + .../theme/widgets/GenAIModelWidget.tsx | 126 ++++++++++++++++++ .../theme/widgets/GenAIRolesWidget.tsx | 2 +- 5 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 web/src/components/config-form/theme/widgets/GenAIModelWidget.tsx diff --git a/frigate/api/app.py b/frigate/api/app.py index f3cc472a88..af67784519 100644 --- a/frigate/api/app.py +++ b/frigate/api/app.py @@ -125,6 +125,16 @@ def metrics(request: Request): return Response(content=content, media_type=content_type) +@router.get( + "/genai/models", + dependencies=[Depends(allow_any_authenticated())], + summary="List available GenAI models", + description="Returns available models for each configured GenAI provider.", +) +def genai_models(request: Request): + return JSONResponse(content=request.app.genai_manager.list_models()) + + @router.get("/config", dependencies=[Depends(allow_any_authenticated())]) def config(request: Request): config_obj: FrigateConfig = request.app.frigate_config diff --git a/web/src/components/config-form/section-configs/genai.ts b/web/src/components/config-form/section-configs/genai.ts index e37478f11e..06a4551d0e 100644 --- a/web/src/components/config-form/section-configs/genai.ts +++ b/web/src/components/config-form/section-configs/genai.ts @@ -37,6 +37,7 @@ const genai: SectionConfigOverrides = { "ui:options": { size: "lg" }, }, "*.model": { + "ui:widget": "genaiModel", "ui:options": { size: "xs" }, }, "*.provider": { diff --git a/web/src/components/config-form/theme/frigateTheme.ts b/web/src/components/config-form/theme/frigateTheme.ts index f1c241c1d8..be9712aee5 100644 --- a/web/src/components/config-form/theme/frigateTheme.ts +++ b/web/src/components/config-form/theme/frigateTheme.ts @@ -24,6 +24,7 @@ import { ReviewLabelSwitchesWidget } from "./widgets/ReviewLabelSwitchesWidget"; import { ZoneSwitchesWidget } from "./widgets/ZoneSwitchesWidget"; import { ArrayAsTextWidget } from "./widgets/ArrayAsTextWidget"; import { FfmpegArgsWidget } from "./widgets/FfmpegArgsWidget"; +import { GenAIModelWidget } from "./widgets/GenAIModelWidget"; import { GenAIRolesWidget } from "./widgets/GenAIRolesWidget"; import { InputRolesWidget } from "./widgets/InputRolesWidget"; import { TimezoneSelectWidget } from "./widgets/TimezoneSelectWidget"; @@ -64,6 +65,7 @@ export const frigateTheme: FrigateTheme = { ArrayAsTextWidget: ArrayAsTextWidget, FfmpegArgsWidget: FfmpegArgsWidget, CameraPathWidget: CameraPathWidget, + genaiModel: GenAIModelWidget, genaiRoles: GenAIRolesWidget, inputRoles: InputRolesWidget, // Custom widgets diff --git a/web/src/components/config-form/theme/widgets/GenAIModelWidget.tsx b/web/src/components/config-form/theme/widgets/GenAIModelWidget.tsx new file mode 100644 index 0000000000..2f35f113e5 --- /dev/null +++ b/web/src/components/config-form/theme/widgets/GenAIModelWidget.tsx @@ -0,0 +1,126 @@ +// Combobox widget for genai *.model fields. +// Fetches available models from the provider's backend and shows them in a dropdown. +import { useState, useMemo } from "react"; +import type { WidgetProps } from "@rjsf/utils"; +import { useTranslation } from "react-i18next"; +import useSWR from "swr"; +import { Check, ChevronsUpDown } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { getSizedFieldClassName } from "../utils"; + +/** + * Extract the provider config entry name from the RJSF widget id. + * Widget ids look like "root_myProvider_model". + */ +function getProviderKey(widgetId: string): string | undefined { + const prefix = "root_"; + const suffix = "_model"; + + if (!widgetId.startsWith(prefix) || !widgetId.endsWith(suffix)) { + return undefined; + } + + return widgetId.slice(prefix.length, -suffix.length) || undefined; +} + +export function GenAIModelWidget(props: WidgetProps) { + const { id, value, disabled, readonly, onChange, options } = props; + const { t } = useTranslation(["views/settings"]); + const [open, setOpen] = useState(false); + + const fieldClassName = getSizedFieldClassName(options, "sm"); + const providerKey = useMemo(() => getProviderKey(id), [id]); + + const { data: allModels } = useSWR>( + "genai/models", + { revalidateOnFocus: false }, + ); + + const models = useMemo(() => { + if (!allModels || !providerKey) return []; + return allModels[providerKey] ?? []; + }, [allModels, providerKey]); + + const currentLabel = typeof value === "string" && value ? value : undefined; + + return ( + + + + + + + + + {models.length > 0 ? ( + + {models.map((model) => ( + { + onChange(model); + setOpen(false); + }} + > + + {model} + + ))} + + ) : ( +
+ {t("configForm.genaiModel.noModels", { + ns: "views/settings", + defaultValue: "No models available", + })} +
+ )} +
+
+
+
+ ); +} diff --git a/web/src/components/config-form/theme/widgets/GenAIRolesWidget.tsx b/web/src/components/config-form/theme/widgets/GenAIRolesWidget.tsx index 92b265b7d8..2d065faead 100644 --- a/web/src/components/config-form/theme/widgets/GenAIRolesWidget.tsx +++ b/web/src/components/config-form/theme/widgets/GenAIRolesWidget.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"; import { Switch } from "@/components/ui/switch"; import type { ConfigFormContext } from "@/types/configForm"; -const GENAI_ROLES = ["embeddings", "vision", "tools"] as const; +const GENAI_ROLES = ["embeddings", "descriptions", "chat"] as const; function normalizeValue(value: unknown): string[] { if (Array.isArray(value)) {