Miscellaneous fixes (0.17 beta) (#21607)
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions

* Strip model name before training

* Handle options file for go2rtc option

* Make reviewed optional and add null to API call

* Send reviewed for dashboard

* Allow setting context size for openai compatible endpoints

* push empty go2rtc config to avoid homekit error in log

* Add option to set runtime options for LLM providers

* Docs

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
Nicolas Mowen
2026-01-12 20:36:38 -07:00
committed by GitHub
co-authored by Josh Hawkins
parent 91cc6747b6
commit 2c34e1ec10
14 changed files with 99 additions and 20 deletions
@@ -10,7 +10,7 @@ class ReviewQueryParams(BaseModel):
cameras: str = "all"
labels: str = "all"
zones: str = "all"
reviewed: int = 0
reviewed: Union[int, SkipJsonSchema[None]] = None
limit: Union[int, SkipJsonSchema[None]] = None
severity: Union[SeverityEnum, SkipJsonSchema[None]] = None
before: Union[float, SkipJsonSchema[None]] = None
+3
View File
@@ -26,3 +26,6 @@ class GenAIConfig(FrigateBaseModel):
provider_options: dict[str, Any] = Field(
default={}, title="GenAI Provider extra options."
)
runtime_options: dict[str, Any] = Field(
default={}, title="Options to pass during inference calls."
)
+1
View File
@@ -64,6 +64,7 @@ class OpenAIClient(GenAIClient):
},
],
timeout=self.timeout,
**self.genai_config.runtime_options,
)
except Exception as e:
logger.warning("Azure OpenAI returned an error: %s", str(e))
+5 -1
View File
@@ -35,10 +35,14 @@ class GeminiClient(GenAIClient):
for img in images
] + [prompt]
try:
# Merge runtime_options into generation_config if provided
generation_config_dict = {"candidate_count": 1}
generation_config_dict.update(self.genai_config.runtime_options)
response = self.provider.generate_content(
data,
generation_config=genai.types.GenerationConfig(
candidate_count=1,
**generation_config_dict
),
request_options=genai.types.RequestOptions(
timeout=self.timeout,
+5 -1
View File
@@ -58,11 +58,15 @@ class OllamaClient(GenAIClient):
)
return None
try:
ollama_options = {
**self.provider_options,
**self.genai_config.runtime_options,
}
result = self.provider.generate(
self.genai_config.model,
prompt,
images=images if images else None,
**self.provider_options,
**ollama_options,
)
logger.debug(
f"Ollama tokens used: eval_count={result.get('eval_count')}, prompt_eval_count={result.get('prompt_eval_count')}"
+19 -3
View File
@@ -22,9 +22,14 @@ class OpenAIClient(GenAIClient):
def _init_provider(self):
"""Initialize the client."""
return OpenAI(
api_key=self.genai_config.api_key, **self.genai_config.provider_options
)
# Extract context_size from provider_options as it's not a valid OpenAI client parameter
# It will be used in get_context_size() instead
provider_opts = {
k: v
for k, v in self.genai_config.provider_options.items()
if k != "context_size"
}
return OpenAI(api_key=self.genai_config.api_key, **provider_opts)
def _send(self, prompt: str, images: list[bytes]) -> Optional[str]:
"""Submit a request to OpenAI."""
@@ -56,6 +61,7 @@ class OpenAIClient(GenAIClient):
},
],
timeout=self.timeout,
**self.genai_config.runtime_options,
)
if (
result is not None
@@ -73,6 +79,16 @@ class OpenAIClient(GenAIClient):
if self.context_size is not None:
return self.context_size
# First check provider_options for manually specified context size
# This is necessary for llama.cpp and other OpenAI-compatible servers
# that don't expose the configured runtime context size in the API response
if "context_size" in self.genai_config.provider_options:
self.context_size = self.genai_config.provider_options["context_size"]
logger.debug(
f"Using context size {self.context_size} from provider_options for model {self.genai_config.model}"
)
return self.context_size
try:
models = self.provider.models.list()
for model in models.data:
+7
View File
@@ -43,6 +43,7 @@ def write_training_metadata(model_name: str, image_count: int) -> None:
model_name: Name of the classification model
image_count: Number of images used in training
"""
model_name = model_name.strip()
clips_model_dir = os.path.join(CLIPS_DIR, model_name)
os.makedirs(clips_model_dir, exist_ok=True)
@@ -70,6 +71,7 @@ def read_training_metadata(model_name: str) -> dict[str, any] | None:
Returns:
Dictionary with last_training_date and last_training_image_count, or None if not found
"""
model_name = model_name.strip()
clips_model_dir = os.path.join(CLIPS_DIR, model_name)
metadata_path = os.path.join(clips_model_dir, TRAINING_METADATA_FILE)
@@ -95,6 +97,7 @@ def get_dataset_image_count(model_name: str) -> int:
Returns:
Total count of images across all categories
"""
model_name = model_name.strip()
dataset_dir = os.path.join(CLIPS_DIR, model_name, "dataset")
if not os.path.exists(dataset_dir):
@@ -126,6 +129,7 @@ class ClassificationTrainingProcess(FrigateProcess):
"TF_KERAS_MOBILENET_V2_WEIGHTS_URL",
"",
)
model_name = model_name.strip()
super().__init__(
stop_event=None,
priority=PROCESS_PRIORITY_LOW,
@@ -292,6 +296,7 @@ class ClassificationTrainingProcess(FrigateProcess):
def kickoff_model_training(
embeddingRequestor: EmbeddingsRequestor, model_name: str
) -> None:
model_name = model_name.strip()
requestor = InterProcessRequestor()
requestor.send_data(
UPDATE_MODEL_STATE,
@@ -359,6 +364,7 @@ def collect_state_classification_examples(
model_name: Name of the classification model
cameras: Dict mapping camera names to normalized crop coordinates [x1, y1, x2, y2] (0-1)
"""
model_name = model_name.strip()
dataset_dir = os.path.join(CLIPS_DIR, model_name, "dataset")
# Step 1: Get review items for the cameras
@@ -714,6 +720,7 @@ def collect_object_classification_examples(
model_name: Name of the classification model
label: Object label to collect (e.g., "person", "car")
"""
model_name = model_name.strip()
dataset_dir = os.path.join(CLIPS_DIR, model_name, "dataset")
temp_dir = os.path.join(dataset_dir, "temp")
os.makedirs(temp_dir, exist_ok=True)