This commit is contained in:
Josh Hawkins
2026-07-19 23:25:23 +00:00
committed by GitHub
5 changed files with 172 additions and 9 deletions
+4 -2
View File
@@ -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
</TabItem>
<TabItem value="yaml">
@@ -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.
```
</TabItem>
</ConfigTabs>
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.
+1 -1
View File
@@ -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,
+28
View File
@@ -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()
@@ -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: "" },
},
});
});
});
@@ -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]);