"""AI Labs configuration: the feature gate, the vendor key, the ceilings. Three things are resolved here, and each is deliberately *not* stored in this domain: * **The vendor credential.** AI Labs shares the org's single AI/LLM connector (``ai_spending.llm.api_key`` / ``.region``) with the support copilot, the intelligence briefs and the spend summaries. One connector means one place to rotate a key and one place the spend shows up. This domain never asks for its own key. * **The model in force.** Not read from ``ai_spending.llm.model`` — that setting is the *copilot's* choice and pointing this surface at it would make an admin's copilot tuning silently change what employees see in the chat model picker. AI Labs takes its models from :mod:`app.domains.ai_labs.catalog`, with an optional per-capability override for the case where a vendor deprecates a model mid-quarter and an operator needs to re-point it before a deploy. * **The ceilings.** Off by default: the org has vendor credit and the whole point of the surface is to get the capability used. The knobs exist so that "if it ever costs real money" is a settings change rather than a release. Fail-closed: ``ai_labs.enabled`` defaults to ``False``, so the domain ships dark and is turned on deliberately. """ from __future__ import annotations from dataclasses import dataclass from typing import Any from sqlalchemy.ext.asyncio import AsyncSession from app.domains.ai_labs import catalog from app.integrations.service import get_platform_setting #: Product-level switch. Fail-closed. SETTING_ENABLED = "ai_labs.enabled" #: Shared AI/LLM connector — same rows the copilot reads. SETTING_VENDOR_KEY = "ai_spending.llm.api_key" SETTING_VENDOR_REGION = "ai_spending.llm.region" #: ``{"": ""}`` — an escape #: hatch for a vendor deprecation, not a tuning surface. SETTING_MODEL_OVERRIDES = "ai_labs.model_overrides" #: Per-person daily ceilings. ``0`` means unlimited, and every one of them #: defaults to ``0``. SETTING_QUOTA_TOKENS = "ai_labs.quota.daily_tokens" SETTING_QUOTA_IMAGES = "ai_labs.quota.daily_images" SETTING_QUOTA_VIDEO_SECONDS = "ai_labs.quota.daily_video_seconds" SETTING_QUOTA_SPEECH = "ai_labs.quota.daily_speech" SETTING_QUOTA_TRANSCRIBE = "ai_labs.quota.daily_transcribe" #: Days after which a conversation or generation is swept. ``0`` = keep #: until the employee deletes it, which is the default: this is the #: employee's own workspace, and an unannounced expiry would be a surprise. SETTING_RETENTION_DAYS = "ai_labs.retention_days" #: Timezone the daily quota window and the usage-rollup date are computed #: in. A UTC day would reset mid-afternoon in Beijing and mid-morning in #: California, so an employee's "today" has to be a real local day. SETTING_REPORTING_TIMEZONE = "ai_labs.reporting_timezone" DEFAULT_REPORTING_TIMEZONE = "Asia/Shanghai" @dataclass(frozen=True) class Quotas: """Per-person, per-day ceilings. ``0`` means unlimited.""" daily_tokens: int = 0 daily_images: int = 0 daily_video_seconds: int = 0 daily_speech: int = 0 daily_transcribe: int = 0 @property def any_enforced(self) -> bool: return any(( self.daily_tokens, self.daily_images, self.daily_video_seconds, self.daily_speech, self.daily_transcribe, )) @dataclass(frozen=True) class AiLabsConfig: """Everything the request path needs, resolved once per request.""" enabled: bool api_key: str region: str quotas: Quotas retention_days: int reporting_timezone: str model_overrides: dict[str, str] @property def available(self) -> bool: """On **and** actually able to call the vendor.""" return self.enabled and bool(self.api_key) def vendor_chat_model(self, model_key: str) -> str: """Vendor model id for a catalogue chat key, honouring overrides.""" override = self.model_overrides.get(model_key) if override: return override return catalog.chat_model(model_key).vendor_model def vendor_capability_model( self, capability: catalog.GenerationCapability, ) -> str: """Vendor model id for a generation capability, honouring overrides.""" return self.model_overrides.get(capability.key) or capability.vendor_model def _as_int(value: Any, default: int = 0) -> int: """Coerce a settings value to a non-negative int. Settings are JSON, so a value hand-edited through the generic settings admin can arrive as ``"200"`` or ``null`` or ``true``. A quota that silently became ``1`` because ``True`` is an ``int`` would throttle the whole company, so booleans are rejected rather than coerced. """ if isinstance(value, bool): return default try: number = int(value) except (TypeError, ValueError): return default return number if number >= 0 else default async def resolve_config(db: AsyncSession) -> AiLabsConfig: """Read every setting this domain depends on, in one place.""" enabled = bool(await get_platform_setting(SETTING_ENABLED, False, session=db)) raw_key = await get_platform_setting(SETTING_VENDOR_KEY, None, session=db) api_key = raw_key.strip() if isinstance(raw_key, str) else "" raw_region = await get_platform_setting( SETTING_VENDOR_REGION, None, session=db, ) region = ( raw_region.strip().lower() if isinstance(raw_region, str) and raw_region.strip() else "intl" ) raw_overrides = await get_platform_setting( SETTING_MODEL_OVERRIDES, None, session=db, ) overrides: dict[str, str] = {} if isinstance(raw_overrides, dict): for key, value in raw_overrides.items(): if isinstance(key, str) and isinstance(value, str) and value.strip(): overrides[key] = value.strip() raw_tz = await get_platform_setting( SETTING_REPORTING_TIMEZONE, None, session=db, ) timezone = ( raw_tz.strip() if isinstance(raw_tz, str) and raw_tz.strip() else DEFAULT_REPORTING_TIMEZONE ) quotas = Quotas( daily_tokens=_as_int( await get_platform_setting(SETTING_QUOTA_TOKENS, 0, session=db), ), daily_images=_as_int( await get_platform_setting(SETTING_QUOTA_IMAGES, 0, session=db), ), daily_video_seconds=_as_int( await get_platform_setting( SETTING_QUOTA_VIDEO_SECONDS, 0, session=db, ), ), daily_speech=_as_int( await get_platform_setting(SETTING_QUOTA_SPEECH, 0, session=db), ), daily_transcribe=_as_int( await get_platform_setting(SETTING_QUOTA_TRANSCRIBE, 0, session=db), ), ) return AiLabsConfig( enabled=enabled, api_key=api_key, region=region, quotas=quotas, retention_days=_as_int( await get_platform_setting(SETTING_RETENTION_DAYS, 0, session=db), ), reporting_timezone=timezone, model_overrides=overrides, ) async def is_available(db: AsyncSession) -> bool: """``True`` iff AI Labs is on and a vendor key resolves.""" return (await resolve_config(db)).available __all__ = [ "DEFAULT_REPORTING_TIMEZONE", "SETTING_ENABLED", "SETTING_MODEL_OVERRIDES", "SETTING_QUOTA_IMAGES", "SETTING_QUOTA_SPEECH", "SETTING_QUOTA_TOKENS", "SETTING_QUOTA_TRANSCRIBE", "SETTING_QUOTA_VIDEO_SECONDS", "SETTING_REPORTING_TIMEZONE", "SETTING_RETENTION_DAYS", "SETTING_VENDOR_KEY", "SETTING_VENDOR_REGION", "AiLabsConfig", "Quotas", "is_available", "resolve_config", ]