diff --git a/frigate/genai/llama_cpp.py b/frigate/genai/llama_cpp.py index 51e8d160d..e5e9883b8 100644 --- a/frigate/genai/llama_cpp.py +++ b/frigate/genai/llama_cpp.py @@ -238,10 +238,15 @@ class LlamaCppClient(GenAIClient): def list_models(self) -> list[str]: """Return available model IDs from the llama.cpp server.""" - if self.provider is None: + base_url = self.provider or ( + self.genai_config.base_url.rstrip("/") + if self.genai_config.base_url + else None + ) + if base_url is None: return [] try: - response = requests.get(f"{self.provider}/v1/models", timeout=10) + response = requests.get(f"{base_url}/v1/models", timeout=10) response.raise_for_status() models = [] for m in response.json().get("data", []): diff --git a/frigate/genai/ollama.py b/frigate/genai/ollama.py index 7315b6e39..7524d54e3 100644 --- a/frigate/genai/ollama.py +++ b/frigate/genai/ollama.py @@ -134,10 +134,20 @@ class OllamaClient(GenAIClient): def list_models(self) -> list[str]: """Return available model names from the Ollama server.""" - if self.provider is None: - return [] + client = self.provider + if client is None: + # Provider init may have failed due to invalid model, but we can + # still list available models with a fresh client. + if not self.genai_config.base_url: + return [] + try: + client = ApiClient( + host=self.genai_config.base_url, timeout=self.timeout + ) + except Exception: + return [] try: - response = self.provider.list() + response = client.list() return sorted( m.get("name", m.get("model", "")) for m in response.get("models", []) ) diff --git a/web/src/components/config-form/theme/widgets/GenAIModelWidget.tsx b/web/src/components/config-form/theme/widgets/GenAIModelWidget.tsx index bb4cb9fed..3be8c0fe3 100644 --- a/web/src/components/config-form/theme/widgets/GenAIModelWidget.tsx +++ b/web/src/components/config-form/theme/widgets/GenAIModelWidget.tsx @@ -1,6 +1,6 @@ // 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 { useState, useMemo, useEffect, useRef } from "react"; import type { WidgetProps } from "@rjsf/utils"; import { useTranslation } from "react-i18next"; import useSWR from "swr"; @@ -19,6 +19,7 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; +import type { ConfigFormContext } from "@/types/configForm"; import { getSizedFieldClassName } from "../utils"; /** @@ -37,17 +38,45 @@ function getProviderKey(widgetId: string): string | undefined { } export function GenAIModelWidget(props: WidgetProps) { - const { id, value, disabled, readonly, onChange, options } = props; + const { id, value, disabled, readonly, onChange, options, registry } = 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", { + const formContext = registry?.formContext as ConfigFormContext | undefined; + + // Build a fingerprint from the saved config's provider + base_url so the + // SWR key changes (and models are refetched) whenever those fields are saved. + const configFingerprint = useMemo(() => { + if (!providerKey) return ""; + const genai = ( + formContext?.fullConfig as Record | undefined + )?.genai; + if (!genai || typeof genai !== "object" || Array.isArray(genai)) return ""; + const entry = (genai as Record)[providerKey]; + if (!entry || typeof entry !== "object" || Array.isArray(entry)) return ""; + const e = entry as Record; + return `${e.provider ?? ""}|${e.base_url ?? ""}`; + }, [providerKey, formContext?.fullConfig]); + + const { data: allModels, mutate: mutateModels } = useSWR< + Record + >("genai/models", { revalidateOnFocus: false, }); + // Revalidate models when the saved config fingerprint changes (e.g. after + // switching provider or base_url and saving). + const prevFingerprint = useRef(configFingerprint); + useEffect(() => { + if (configFingerprint !== prevFingerprint.current) { + prevFingerprint.current = configFingerprint; + mutateModels(); + } + }, [configFingerprint, mutateModels]); + const models = useMemo(() => { if (!allModels || !providerKey) return []; return allModels[providerKey] ?? [];