Support getting client via manager

This commit is contained in:
Nicolas Mowen 2026-02-14 15:53:56 -07:00
parent 7c066f661a
commit 1e4596eb99
8 changed files with 93 additions and 34 deletions

View File

@ -38,6 +38,7 @@ from frigate.config.camera.updater import (
CameraConfigUpdateTopic, CameraConfigUpdateTopic,
) )
from frigate.ffmpeg_presets import FFMPEG_HWACCEL_VAAPI, _gpu_selector from frigate.ffmpeg_presets import FFMPEG_HWACCEL_VAAPI, _gpu_selector
from frigate.genai import GenAIClientManager
from frigate.jobs.media_sync import ( from frigate.jobs.media_sync import (
get_current_media_sync_job, get_current_media_sync_job,
get_media_sync_job_by_id, get_media_sync_job_by_id,
@ -432,6 +433,7 @@ def config_set(request: Request, body: AppConfigSetBody):
if body.requires_restart == 0 or body.update_topic: if body.requires_restart == 0 or body.update_topic:
old_config: FrigateConfig = request.app.frigate_config old_config: FrigateConfig = request.app.frigate_config
request.app.frigate_config = config request.app.frigate_config = config
request.app.genai_manager = GenAIClientManager(config)
if body.update_topic: if body.update_topic:
if body.update_topic.startswith("config/cameras/"): if body.update_topic.startswith("config/cameras/"):

View File

@ -23,7 +23,6 @@ from frigate.api.defs.response.chat_response import (
) )
from frigate.api.defs.tags import Tags from frigate.api.defs.tags import Tags
from frigate.api.event import events from frigate.api.event import events
from frigate.genai import get_genai_client
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -383,7 +382,7 @@ async def chat_completion(
6. Repeats until final answer 6. Repeats until final answer
7. Returns response to user 7. Returns response to user
""" """
genai_client = get_genai_client(request.app.frigate_config) genai_client = request.app.genai_manager.tool_client
if not genai_client: if not genai_client:
return JSONResponse( return JSONResponse(
content={ content={

View File

@ -33,6 +33,7 @@ from frigate.comms.event_metadata_updater import (
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.config.camera.updater import CameraConfigUpdatePublisher from frigate.config.camera.updater import CameraConfigUpdatePublisher
from frigate.embeddings import EmbeddingsContext from frigate.embeddings import EmbeddingsContext
from frigate.genai import GenAIClientManager
from frigate.ptz.onvif import OnvifController from frigate.ptz.onvif import OnvifController
from frigate.stats.emitter import StatsEmitter from frigate.stats.emitter import StatsEmitter
from frigate.storage import StorageMaintainer from frigate.storage import StorageMaintainer
@ -134,6 +135,7 @@ def create_fastapi_app(
app.include_router(record.router) app.include_router(record.router)
# App Properties # App Properties
app.frigate_config = frigate_config app.frigate_config = frigate_config
app.genai_manager = GenAIClientManager(frigate_config)
app.embeddings = embeddings app.embeddings = embeddings
app.detected_frames_processor = detected_frames_processor app.detected_frames_processor = detected_frames_processor
app.storage_maintainer = storage_maintainer app.storage_maintainer = storage_maintainer

View File

@ -33,7 +33,6 @@ from frigate.api.defs.response.review_response import (
ReviewSummaryResponse, ReviewSummaryResponse,
) )
from frigate.api.defs.tags import Tags from frigate.api.defs.tags import Tags
from frigate.config import FrigateConfig
from frigate.embeddings import EmbeddingsContext from frigate.embeddings import EmbeddingsContext
from frigate.models import Recordings, ReviewSegment, UserReviewStatus from frigate.models import Recordings, ReviewSegment, UserReviewStatus
from frigate.review.types import SeverityEnum from frigate.review.types import SeverityEnum
@ -747,9 +746,7 @@ async def set_not_reviewed(
description="Use GenAI to summarize review items over a period of time.", description="Use GenAI to summarize review items over a period of time.",
) )
def generate_review_summary(request: Request, start_ts: float, end_ts: float): def generate_review_summary(request: Request, start_ts: float, end_ts: float):
config: FrigateConfig = request.app.frigate_config if not request.app.genai_manager.vision_client:
if not config.genai.provider:
return JSONResponse( return JSONResponse(
content=( content=(
{ {

View File

@ -45,7 +45,7 @@ from .camera.audio import AudioConfig
from .camera.birdseye import BirdseyeConfig from .camera.birdseye import BirdseyeConfig
from .camera.detect import DetectConfig from .camera.detect import DetectConfig
from .camera.ffmpeg import FfmpegConfig from .camera.ffmpeg import FfmpegConfig
from .camera.genai import GenAIConfig from .camera.genai import GenAIConfig, GenAIRoleEnum
from .camera.motion import MotionConfig from .camera.motion import MotionConfig
from .camera.notification import NotificationConfig from .camera.notification import NotificationConfig
from .camera.objects import FilterConfig, ObjectConfig from .camera.objects import FilterConfig, ObjectConfig
@ -347,9 +347,9 @@ class FrigateConfig(FrigateBaseModel):
default_factory=ModelConfig, title="Detection model configuration." default_factory=ModelConfig, title="Detection model configuration."
) )
# GenAI config # GenAI config (named provider configs: name -> GenAIConfig)
genai: GenAIConfig = Field( genai: Dict[str, GenAIConfig] = Field(
default_factory=GenAIConfig, title="Generative AI configuration." default_factory=dict, title="Generative AI configuration (named providers)."
) )
# Camera config # Camera config
@ -431,6 +431,18 @@ class FrigateConfig(FrigateBaseModel):
# set notifications state # set notifications state
self.notifications.enabled_in_config = self.notifications.enabled self.notifications.enabled_in_config = self.notifications.enabled
# validate genai: each role (tools, vision, embeddings) at most once
role_to_name: dict[GenAIRoleEnum, str] = {}
for name, genai_cfg in self.genai.items():
for role in genai_cfg.roles:
if role in role_to_name:
raise ValueError(
f"GenAI role '{role.value}' is assigned to both "
f"'{role_to_name[role]}' and '{name}'; each role must have "
"exactly one provider."
)
role_to_name[role] = name
# set default min_score for object attributes # set default min_score for object attributes
for attribute in self.model.all_attributes: for attribute in self.model.all_attributes:
if not self.objects.filters.get(attribute): if not self.objects.filters.get(attribute):

View File

@ -59,7 +59,7 @@ from frigate.data_processing.real_time.license_plate import (
from frigate.data_processing.types import DataProcessorMetrics, PostProcessDataEnum from frigate.data_processing.types import DataProcessorMetrics, PostProcessDataEnum
from frigate.db.sqlitevecq import SqliteVecQueueDatabase from frigate.db.sqlitevecq import SqliteVecQueueDatabase
from frigate.events.types import EventTypeEnum, RegenerateDescriptionEnum from frigate.events.types import EventTypeEnum, RegenerateDescriptionEnum
from frigate.genai import get_genai_client from frigate.genai import GenAIClientManager
from frigate.models import Event, Recordings, ReviewSegment, Trigger from frigate.models import Event, Recordings, ReviewSegment, Trigger
from frigate.util.builtin import serialize from frigate.util.builtin import serialize
from frigate.util.file import get_event_thumbnail_bytes from frigate.util.file import get_event_thumbnail_bytes
@ -144,7 +144,8 @@ class EmbeddingMaintainer(threading.Thread):
self.frame_manager = SharedMemoryFrameManager() self.frame_manager = SharedMemoryFrameManager()
self.detected_license_plates: dict[str, dict[str, Any]] = {} self.detected_license_plates: dict[str, dict[str, Any]] = {}
self.genai_client = get_genai_client(config) self.genai_manager = GenAIClientManager(config)
self.genai_client = self.genai_manager.vision_client
# model runners to share between realtime and post processors # model runners to share between realtime and post processors
if self.config.lpr.enabled: if self.config.lpr.enabled:

View File

@ -23,7 +23,6 @@ __all__ = [
"GenAIConfig", "GenAIConfig",
"GenAIProviderEnum", "GenAIProviderEnum",
"PROVIDERS", "PROVIDERS",
"get_genai_client",
"load_providers", "load_providers",
"register_genai_provider", "register_genai_provider",
] ]
@ -364,19 +363,6 @@ Guidelines:
} }
def get_genai_client(config: FrigateConfig) -> Optional[GenAIClient]:
"""Get the GenAI client."""
if not config.genai.provider:
return None
load_providers()
provider = PROVIDERS.get(config.genai.provider)
if provider:
return provider(config.genai)
return None
def load_providers(): def load_providers():
package_dir = os.path.dirname(__file__) package_dir = os.path.dirname(__file__)
for filename in os.listdir(package_dir): for filename in os.listdir(package_dir):

View File

@ -1,13 +1,19 @@
"""GenAI client manager for Frigate. """GenAI client manager for Frigate.
Manages GenAI provider clients and supports multiple providers. Configuration Manages GenAI provider clients from Frigate config. Configuration is read only
is driven by FrigateConfig; _update_config is called on init and can be in _update_config(); no other code should read config.genai. Exposes clients
called again when config is reloaded. by role: tool_client, vision_client, embeddings_client.
""" """
import logging import logging
from typing import TYPE_CHECKING, Optional
from frigate.config import FrigateConfig from frigate.config import FrigateConfig
from frigate.config.camera.genai import GenAIRoleEnum
from frigate.genai import PROVIDERS, load_providers
if TYPE_CHECKING:
from frigate.genai import GenAIClient
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -17,13 +23,67 @@ class GenAIClientManager:
def __init__(self, config: FrigateConfig) -> None: def __init__(self, config: FrigateConfig) -> None:
self._config = config self._config = config
self._tool_client: Optional[GenAIClient] = None
self._vision_client: Optional[GenAIClient] = None
self._embeddings_client: Optional[GenAIClient] = None
self._update_config() self._update_config()
def _update_config(self) -> None: def _update_config(self) -> None:
"""Update internal state from the current Frigate config. """Build role clients from current Frigate config.genai.
Called from __init__ and can be called again when config is reloaded Called from __init__ and can be called again when config is reloaded.
to support multiple providers or config changes. Each role (tools, vision, embeddings) gets the client for the provider
that has that role in its roles list.
""" """
# Placeholder for multi-provider setup; will be extended in later refactor steps.
pass self._tool_client = None
self._vision_client = None
self._embeddings_client = None
if not self._config.genai:
return
load_providers()
for _name, genai_cfg in self._config.genai.items():
if not genai_cfg.provider:
continue
provider_cls = PROVIDERS.get(genai_cfg.provider)
if not provider_cls:
logger.warning(
"Unknown GenAI provider %s in config, skipping.",
genai_cfg.provider,
)
continue
try:
client = provider_cls(genai_cfg)
except Exception as e:
logger.exception(
"Failed to create GenAI client for provider %s: %s",
genai_cfg.provider,
e,
)
continue
for role in genai_cfg.roles:
if role == GenAIRoleEnum.tools:
self._tool_client = client
elif role == GenAIRoleEnum.vision:
self._vision_client = client
elif role == GenAIRoleEnum.embeddings:
self._embeddings_client = client
@property
def tool_client(self) -> "Optional[GenAIClient]":
"""Client configured for the tools role (e.g. chat with function calling)."""
return self._tool_client
@property
def vision_client(self) -> "Optional[GenAIClient]":
"""Client configured for the vision role (e.g. review descriptions, object descriptions)."""
return self._vision_client
@property
def embeddings_client(self) -> "Optional[GenAIClient]":
"""Client configured for the embeddings role."""
return self._embeddings_client