From ce0197cc8f93969dbe86be2d4e5ec6b29dbeb176 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 19 Jul 2026 06:47:08 -0500 Subject: [PATCH 1/3] fix semantic search model_size showing dirty for genai embeddings providers The resolved config always reports model_size (schema default), so clearing it whenever a provider was selected falsely marked the field dirty on load and sent a delete for a YAML key that isn't there (Error updating config: 'model_size'). Only clear a non-default value, which is the only case actually present in the config file. --- .../specs/settings/semantic-search.spec.ts | 129 ++++++++++++++++++ .../widgets/SemanticSearchModelSizeWidget.tsx | 16 ++- 2 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 web/e2e/specs/settings/semantic-search.spec.ts diff --git a/web/e2e/specs/settings/semantic-search.spec.ts b/web/e2e/specs/settings/semantic-search.spec.ts new file mode 100644 index 0000000000..b7074800a2 --- /dev/null +++ b/web/e2e/specs/settings/semantic-search.spec.ts @@ -0,0 +1,129 @@ +/** + * Semantic Search settings tests -- MEDIUM tier. + * + * Focuses on the model_size field, which is unused when a GenAI embeddings + * provider is selected as the semantic search model. The resolved config always + * reports model_size (it has a schema default of "small"), even when the YAML + * file has no such key. Clearing model_size for a provider used to run + * unconditionally, which falsely marked the section dirty on load and asked the + * backend to delete a key that wasn't in the config file (KeyError: 'model_size'). + */ + +import { readFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { test, expect } from "../../fixtures/frigate-test"; +import type { Page } from "@playwright/test"; +import { configFactory } from "../../fixtures/mock-data/config"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const CONFIG_SCHEMA = JSON.parse( + readFileSync( + resolve(__dirname, "../../fixtures/mock-data/config-schema.json"), + "utf-8", + ), +); + +const PROVIDER = "llama_cpp"; +const SETTINGS_URL = "/settings?page=integrationSemanticSearch"; +const NOT_APPLICABLE = "Not applicable for GenAI providers"; +const UNSAVED = "You have unsaved changes"; + +type SemanticSearch = { + enabled?: boolean; + model?: string; + model_size?: string; +}; + +async function installRoutes(page: Page, semanticSearch: SemanticSearch) { + const config = configFactory({ + genai: { [PROVIDER]: { provider: PROVIDER, roles: ["embeddings"] } }, + semantic_search: semanticSearch, + }); + + let lastSavedConfig: unknown = null; + + await page.route("**/api/config/schema.json", (route) => + route.fulfill({ json: CONFIG_SCHEMA }), + ); + await page.route("**/api/config", (route) => { + if (route.request().method() === "GET") { + return route.fulfill({ json: config }); + } + return route.fulfill({ json: { success: true } }); + }); + await page.route("**/api/config/set", async (route) => { + lastSavedConfig = route.request().postDataJSON(); + await route.fulfill({ json: { success: true, require_restart: false } }); + }); + await page.route("**/api/config/raw_paths", (route) => + route.fulfill({ json: { semantic_search: semanticSearch } }), + ); + + return { capturedConfig: () => lastSavedConfig }; +} + +test.describe("semantic search model_size @medium", () => { + test("a provider with a defaulted model_size is not dirty on load", async ({ + frigateApp, + }) => { + // model_size stays at its schema default ("small"), i.e. it is not present + // in the YAML. This mirrors the reported bug: selecting a GenAI provider and + // returning to the page. + await installRoutes(frigateApp.page, { + enabled: true, + model: PROVIDER, + }); + await frigateApp.goto(SETTINGS_URL); + + // The provider path is active: model_size shows "Not applicable". + await expect(frigateApp.page.getByText(NOT_APPLICABLE)).toBeVisible(); + + // Give any clearing effect time to fire, then confirm the section stayed + // clean (no phantom unsaved-changes banner, Save disabled). + await frigateApp.page.waitForTimeout(1000); + await expect(frigateApp.page.getByText(UNSAVED)).toBeHidden(); + await expect( + frigateApp.page.getByRole("button", { name: "Save", exact: true }), + ).toBeDisabled(); + }); + + test("switching from a configured non-default model_size clears it", async ({ + frigateApp, + }) => { + // A genuinely configured non-default model_size ("large") can only come from + // the YAML, so switching to a provider must still remove it. + const capture = await installRoutes(frigateApp.page, { + enabled: true, + model: "jinav2", + model_size: "large", + }); + await frigateApp.goto(SETTINGS_URL); + + // Starts clean on a Jina model. + await expect(frigateApp.page.getByText(UNSAVED)).toBeHidden(); + + // Switch the model to the GenAI provider. + await frigateApp.page + .getByRole("combobox", { name: /Semantic search model/ }) + .click(); + await frigateApp.page.getByRole("option", { name: PROVIDER }).click(); + + // The change is now dirty and model_size is no longer applicable. + await expect(frigateApp.page.getByText(NOT_APPLICABLE)).toBeVisible(); + await expect(frigateApp.page.getByText(UNSAVED)).toBeVisible(); + + await frigateApp.page + .getByRole("button", { name: "Save", exact: true }) + .click(); + + // The saved payload removes model_size (empty string = "remove" key). + await expect + .poll(() => capture.capturedConfig(), { timeout: 5_000 }) + .toMatchObject({ + config_data: { + semantic_search: { model: PROVIDER, model_size: "" }, + }, + }); + }); +}); diff --git a/web/src/components/config-form/theme/widgets/SemanticSearchModelSizeWidget.tsx b/web/src/components/config-form/theme/widgets/SemanticSearchModelSizeWidget.tsx index 4ee0019363..6e8f383cae 100644 --- a/web/src/components/config-form/theme/widgets/SemanticSearchModelSizeWidget.tsx +++ b/web/src/components/config-form/theme/widgets/SemanticSearchModelSizeWidget.tsx @@ -24,15 +24,19 @@ export function SemanticSearchModelSizeWidget(props: WidgetProps) { model !== "jinav1" && model !== "jinav2"; - // Clear model_size while on a provider (buildOverrides converts to "" - // which the backend treats as "remove"). Restore the schema default - // when returning to a Jina model so the field isn't left empty. + // model_size is unused on a GenAI provider. Only clear it (which the backend + // treats as "remove") for a non-default value, which can only come from the + // config file. A defaulted value is indistinguishable from unset in the + // resolved config, so clearing it would falsely dirty the field and delete a + // YAML key that isn't there. Restore the default when returning to a Jina model. const { value, onChange, schema } = props; const schemaDefault = schema?.default as string | undefined; useEffect(() => { - if (isProvider && value !== undefined) { - onChange(undefined); - } else if (!isProvider && value === undefined && schemaDefault) { + if (isProvider) { + if (value !== undefined && value !== schemaDefault) { + onChange(undefined); + } + } else if (value === undefined && schemaDefault) { onChange(schemaDefault); } }, [isProvider, value, onChange, schemaDefault]); From 761a431c55ee4e1c2f41203f5456895858f9a1a7 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sun, 19 Jul 2026 17:22:33 -0600 Subject: [PATCH 2/3] Use manual context size if set --- frigate/genai/plugins/llama_cpp.py | 2 +- frigate/test/test_genai_providers.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/frigate/genai/plugins/llama_cpp.py b/frigate/genai/plugins/llama_cpp.py index af3ecc9b18..a217e0d898 100644 --- a/frigate/genai/plugins/llama_cpp.py +++ b/frigate/genai/plugins/llama_cpp.py @@ -192,7 +192,7 @@ class LlamaCppClient(GenAIClient): logger.info( "llama.cpp model '%s' initialized — context: %s, vision: %s, audio: %s, tools: %s, reasoning: %s", configured_model, - self._context_size or "unknown", + self.get_context_size(), self._supports_vision, self._supports_audio, self._supports_tools, diff --git a/frigate/test/test_genai_providers.py b/frigate/test/test_genai_providers.py index d73d632f0a..5352a81e25 100644 --- a/frigate/test/test_genai_providers.py +++ b/frigate/test/test_genai_providers.py @@ -491,6 +491,34 @@ class TestLlamaCppProvider(unittest.TestCase): final = _final_message(self._run_with_lines(client, lines, MULTIMODAL_MESSAGES)) self.assertEqual(final["content"], "ok") + def _validated_client(self, server_context_size, provider_options=None): + """Build a client as if the server reported the given context size.""" + cfg = GenAIConfig( + provider="llamacpp", + model="m", + base_url="http://localhost:9999", + provider_options=provider_options or {}, + ) + info = { + "context_size": server_context_size, + "supports_vision": False, + "supports_audio": False, + "supports_tools": False, + "supports_reasoning": False, + "media_marker": "<__media__>", + } + cls = PROVIDERS[GenAIProviderEnum.llamacpp] + with patch.object(cls, "_get_model_info", return_value=info): + return cls(cfg, timeout=5) + + def test_server_context_size_used_without_override(self): + client = self._validated_client(4096) + self.assertEqual(client.get_context_size(), 4096) + + def test_provider_options_context_size_overrides_server(self): + client = self._validated_client(4096, {"context_size": 32768}) + self.assertEqual(client.get_context_size(), 32768) + if __name__ == "__main__": unittest.main() From 45d3c6389e946f8c6d8fdf47812c021855af39bc Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sun, 19 Jul 2026 17:25:16 -0600 Subject: [PATCH 3/3] update docs --- docs/docs/configuration/genai/config.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/docs/configuration/genai/config.md b/docs/docs/configuration/genai/config.md index 738eb5db3f..d1da92c05f 100644 --- a/docs/docs/configuration/genai/config.md +++ b/docs/docs/configuration/genai/config.md @@ -78,7 +78,7 @@ All llama.cpp native options can be passed through `provider_options`, including - Set **Provider** to `llamacpp` - Set **Base URL** to your llama.cpp server address (e.g., `http://localhost:8080`) - Set **Model** to the name of your model - - Under **Provider Options**, set `context_size` to tell Frigate your context size so it can send the appropriate amount of information + - Optionally, under **Provider Options**, set `context_size` to override the context size Frigate detects from the server @@ -89,12 +89,14 @@ genai: base_url: http://localhost:8080 model: your-model-name provider_options: - context_size: 16000 # Tell Frigate your context size so it can send the appropriate amount of information. + context_size: 16000 # Optional, overrides the context size reported by the server. ``` +Frigate queries the llama.cpp server for the model's context size at startup and logs it along with the other detected capabilities. If `context_size` is set in `provider_options`, that value is always used instead, even when the server reports its own. + ### Ollama [Ollama](https://ollama.com/) allows you to self-host large language models and keep everything running locally. It is highly recommended to host this server on a machine with an Nvidia graphics card, or on a Apple silicon Mac for best performance.