Files

1 line
140 KiB
JSON

{"repo": "apocas/restai", "files": {"restai/models/models.py": {"38e9cab3674e07fc83c99555e322f759ed9be6a0": {"t": 1785674619.2858915, "n": 137099, "h": "99d2c91f6a7de28d", "c": "import os\nimport re\n\nfrom pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator\nfrom typing import Any, Dict, List, Literal, Optional, Union\nimport json\nfrom datetime import datetime\n\n\n_SAFE_NAME_RE = re.compile(r'^[a-zA-Z0-9._:-]+$')\n\nVALID_LLM_CLASSES = {\n \"Ollama\", \"OllamaCloud\", \"OllamaMultiModal\", \"OllamaMultiModal2\",\n \"OpenAI\", \"OpenAILike\", \"Grok\", \"Anthropic\", \"LiteLLM\", \"vLLM\",\n \"GeminiMultiModal\", \"Gemini\", \"AzureOpenAI\", \"Bedrock\",\n}\n\nVALID_EMBEDDING_CLASSES = {\n \"LangChain\", \"LangChain.Openai\", \"LangChain.HuggingFace\",\n \"OllamaEmbeddings\", \"Ollama\",\n}\n\n# Providers for the image-generator registry. `local` covers anything in\n# `restai/image/workers/*.py` (auto-seeded on startup); `openai` covers\n# both api.openai.com and any OpenAI-spec-compatible server (Together,\n# Groq, Fireworks, vLLM image servers, etc.) — set `options.base_url` on\n# the latter. `google` covers Imagen / Nano Banana via the Google AI SDK.\nVALID_IMAGE_GENERATOR_CLASSES = {\"local\", \"openai\", \"google\"}\n\n# Providers for the speech-to-text registry. `local` covers\n# `restai/audio/workers/*.py` (Whisper / WhisperX / Granite Speech, etc.).\n# External providers: `openai` (Whisper API + OpenAI-compat via\n# options.base_url), `google` (Cloud Speech-to-Text), `deepgram`,\n# `assemblyai`.\nVALID_SPEECH_TO_TEXT_CLASSES = {\"local\", \"openai\", \"google\", \"deepgram\", \"assemblyai\"}\n\n\ndef validate_safe_name(v: str, field_label: str = \"Name\") -> str:\n \"\"\"Reject names containing characters unsafe for use in URL paths.\"\"\"\n if not v or not v.strip():\n raise ValueError(f\"{field_label} cannot be empty\")\n if not _SAFE_NAME_RE.match(v):\n raise ValueError(\n f\"{field_label} can only contain letters, numbers, hyphens, underscores, dots, and colons\"\n )\n return v\n\n\ndef sanitize_filename(filename: str) -> str:\n \"\"\"Strip path components and dangerous characters from uploaded filenames.\"\"\"\n name = os.path.basename(filename)\n name = name.replace('\\x00', '')\n return name if name.strip() else 'unnamed_file'\n\n\nclass URLIngestModel(BaseModel):\n \"\"\"Ingest a web page into a RAG project's knowledge base.\"\"\"\n url: str = Field(max_length=2000, description=\"URL of the web page to ingest\")\n splitter: Literal[\"sentence\", \"token\"] = Field(default=\"sentence\", description=\"Text splitting strategy: 'sentence' or 'token'\")\n chunks: int = Field(default=512, ge=32, le=8192, description=\"Maximum chunk size in characters or tokens\")\n\n @field_validator('url')\n @classmethod\n def url_must_be_http(cls, v):\n if not v.startswith(('http://', 'https://')):\n raise ValueError(\"URL must start with http:// or https://\")\n return v\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"url\": \"https://example.com/docs/getting-started\",\n \"splitter\": \"sentence\",\n \"chunks\": 512\n }\n })\n\n\nclass TextIngestModel(BaseModel):\n \"\"\"Ingest raw text into a RAG project's knowledge base.\"\"\"\n text: str = Field(max_length=10_000_000, description=\"Raw text content to ingest\")\n source: str = Field(max_length=500, description=\"Source identifier for the ingested text\")\n splitter: Literal[\"sentence\", \"token\"] = Field(default=\"sentence\", description=\"Text splitting strategy: 'sentence' or 'token'\")\n chunks: int = Field(default=512, ge=32, le=8192, description=\"Maximum chunk size in characters or tokens\")\n keywords: Union[list[str], None] = Field(default=None, description=\"Optional keywords to associate with the ingested text\")\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"text\": \"RESTai is an AIaaS platform that allows you to create AI projects.\",\n \"source\": \"internal-docs\",\n \"splitter\": \"sentence\",\n \"chunks\": 512,\n \"keywords\": [\"restai\", \"platform\", \"ai\"]\n }\n })\n\n\nclass FindModel(BaseModel):\n \"\"\"Search for embeddings by source or text similarity.\"\"\"\n source: Union[str, None] = Field(default=None, description=\"Filter results by source identifier\")\n text: Union[str, None] = Field(default=None, description=\"Text query for similarity search\")\n score: Union[float, None] = Field(default=0.0, description=\"Minimum similarity score threshold (0.0 to 1.0)\")\n k: Union[int, None] = Field(default=None, ge=1, le=100, description=\"Maximum number of results to return\")\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"text\": \"How do I configure embeddings?\",\n \"score\": 0.3,\n \"k\": 5\n }\n })\n\n\nclass InteractionModel(BaseModel):\n \"\"\"Base model for chat and question interactions.\"\"\"\n question: str = Field(max_length=100000, description=\"The user's question or prompt\")\n stream: Union[bool, None] = Field(default=None, description=\"Enable streaming response (server-sent events)\")\n context: Union[Dict[str, Any], None] = Field(default=None, description=\"Context variables to inject into the system prompt via {{context.key}} placeholders\")\n\n\nclass ImageModel(BaseModel):\n \"\"\"Image generation request.\"\"\"\n prompt: str = Field(max_length=10000, description=\"Text prompt describing the image to generate\")\n image: Union[str, None] = Field(default=None, description=\"Base64-encoded input image for image-to-image generation\")\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"prompt\": \"A serene mountain landscape at sunset with a lake reflection\"\n }\n })\n\n\nclass FileAttachment(BaseModel):\n \"\"\"A user-uploaded file attached to a question or chat message. The file\n will be dropped into the agent's sandbox container at\n ``/home/user/uploads/<name>`` so the LLM can manipulate it with the\n terminal tool.\"\"\"\n name: str = Field(max_length=255, description=\"Filename (path components are stripped server-side)\")\n # 70 MB base64 string ≈ 50 MB raw bytes. Symmetric with the 10 MB cap on\n # `ChatModel.image` (which includes URL fetch + SSRF guard via\n # resolve_image); for base64 attachments the only useful layer is this\n # string-length bound — otherwise a 500 MB payload OOMs the worker\n # before any code runs.\n content: str = Field(max_length=70_000_000, description=\"Base64-encoded file bytes (max ~50 MB raw)\")\n mime_type: Union[str, None] = Field(default=None, max_length=100, description=\"Optional MIME type hint (e.g. application/pdf)\")\n\n @field_validator(\"name\")\n @classmethod\n def name_must_be_safe(cls, v):\n # The bytes are extracted into the sandbox via tar. Strip path\n # components + null bytes so a crafted name (e.g. \"../../etc/x\")\n # can't traverse out of the uploads dir — makes the docstring's\n # \"path components are stripped server-side\" promise actually true.\n return sanitize_filename(v)\n\n\n_CHAT_ID_RE = re.compile(r\"^[a-zA-Z0-9_-]+$\")\n\n\nclass ChatModel(InteractionModel):\n \"\"\"Send a chat message (or one-shot question) to a project. Maintains\n conversation state via the ``id`` field; omit ``id`` for an ephemeral\n one-shot turn.\"\"\"\n id: Union[str, None] = Field(default=None, max_length=128, description=\"Conversation ID for maintaining chat state. Omit to start a new conversation (ephemeral one-shot turn).\")\n image: Union[str, None] = Field(default=None, description=\"Base64-encoded image for vision models\")\n files: Union[list[FileAttachment], None] = Field(default=None, max_length=10, description=\"Up to 10 files to upload into the agent sandbox at /home/user/uploads/\")\n system: Union[str, None] = Field(default=None, description=\"System prompt override for this turn (Agent + RAG)\")\n llm_rerank: Union[bool, None] = Field(default=None, description=\"Enable LLM-based reranking of retrieved documents (RAG only)\")\n tables: Union[list[str], None] = Field(default=None, description=\"Restrict NL→SQL to specific tables (RAG with connection only)\")\n negative: Union[str, None] = Field(default=None, description=\"Negative prompt to steer the response away from certain content (RAG only)\")\n lite: bool = Field(default=False, description=\"Return a lightweight response without full source details (RAG only)\")\n eval: bool = Field(default=False, description=\"Enable response evaluation and scoring (RAG only, non-streaming)\")\n k: Optional[int] = Field(None, ge=1, le=25, description=\"Number of documents to retrieve from the knowledge base (RAG only)\")\n score: Union[float, None] = Field(default=None, description=\"Minimum similarity score threshold for retrieved documents (RAG only)\")\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"question\": \"Tell me about the RAG project type.\",\n \"stream\": False,\n \"id\": \"conv_abc123\"\n }\n })\n\n @field_validator(\"id\")\n @classmethod\n def validate_chat_id(cls, v):\n if v is not None and not _CHAT_ID_RE.match(v):\n raise ValueError(\"Chat ID must contain only alphanumeric characters, hyphens, and underscores\")\n return v\n\n\nclass LLMModel(BaseModel):\n \"\"\"LLM provider configuration.\"\"\"\n id: Union[int, None] = Field(default=None, description=\"Unique LLM identifier\")\n name: str = Field(description=\"Unique name identifier for the LLM\")\n class_name: str = Field(description=\"LLM implementation class (e.g. 'ollama', 'openai', 'anthropic', 'litellm')\")\n options: Dict[str, Any] = Field(description=\"Provider-specific configuration options (model name, API keys, etc.)\")\n privacy: Literal[\"public\", \"private\"] = Field(description=\"Privacy level: 'public' (cloud-hosted) or 'private' (self-hosted)\")\n description: Union[str, None] = Field(default=None, max_length=1000, description=\"Human-readable description of the LLM\")\n input_cost: float = Field(default=0.0, ge=0, description=\"Cost per 1M input tokens in configured currency\")\n output_cost: float = Field(default=0.0, ge=0, description=\"Cost per 1M output tokens in configured currency\")\n context_window: Union[int, None] = Field(default=4096, ge=1, le=10000000, description=\"Maximum context window size in tokens\")\n teams: list[\"TeamModel\"] = Field(default=[], description=\"Teams that have access to this LLM\")\n model_config = ConfigDict(from_attributes=True, json_schema_extra={\n \"example\": {\n \"name\": \"gpt-4o\",\n \"class_name\": \"litellm\",\n \"options\": {\"model\": \"openai/gpt-4o\"},\n \"privacy\": \"public\",\n \"description\": \"OpenAI GPT-4o model\",\n \"input_cost\": 5.0,\n \"output_cost\": 15.0,\n \"context_window\": 128000\n }\n })\n\n @field_validator('name')\n @classmethod\n def name_must_be_safe(cls, v):\n return validate_safe_name(v, \"LLM name\")\n\n @field_validator('class_name')\n @classmethod\n def class_name_must_be_valid(cls, v):\n if v not in VALID_LLM_CLASSES:\n raise ValueError(f\"Invalid LLM class: '{v}'. Must be one of: {', '.join(sorted(VALID_LLM_CLASSES))}\")\n return v\n\n @field_validator('options', mode='before')\n @classmethod\n def parse_options(cls, v):\n if isinstance(v, str):\n try:\n v = json.loads(v)\n except json.JSONDecodeError:\n return {}\n if isinstance(v, dict):\n try:\n from restai.utils.crypto import decrypt_sensitive_options, LLM_SENSITIVE_KEYS\n v = decrypt_sensitive_options(v, LLM_SENSITIVE_KEYS)\n except Exception:\n pass\n return v\n\n\nclass EmbeddingModel(BaseModel):\n \"\"\"Embedding model provider configuration.\"\"\"\n id: Union[int, None] = Field(default=None, description=\"Unique embedding model identifier\")\n name: str = Field(description=\"Unique name identifier for the embedding model\")\n class_name: str = Field(description=\"Embedding implementation class (e.g. 'ollama', 'openai')\")\n options: str = Field(description=\"JSON string of provider-specific configuration options\")\n privacy: Literal[\"public\", \"private\"] = Field(description=\"Privacy level: 'public' (cloud-hosted) or 'private' (self-hosted)\")\n description: Union[str, None] = Field(default=None, max_length=1000, description=\"Human-readable description of the embedding model\")\n dimension: int = Field(default=1536, ge=1, le=65536, description=\"Output embedding dimension size\")\n teams: list[\"TeamModel\"] = Field(default=[], description=\"Teams that have access to this embedding model\")\n\n @field_validator('name')\n @classmethod\n def name_must_be_safe(cls, v):\n return validate_safe_name(v, \"Embedding name\")\n\n @field_validator('class_name')\n @classmethod\n def class_name_must_be_valid(cls, v):\n if v not in VALID_EMBEDDING_CLASSES:\n raise ValueError(f\"Invalid embedding class: '{v}'. Must be one of: {', '.join(sorted(VALID_EMBEDDING_CLASSES))}\")\n return v\n\n model_config = ConfigDict(from_attributes=True, json_schema_extra={\n \"example\": {\n \"name\": \"text-embedding-3-small\",\n \"class_name\": \"openai\",\n \"options\": \"{\\\"model\\\": \\\"text-embedding-3-small\\\"}\",\n \"privacy\": \"public\",\n \"description\": \"OpenAI small embedding model\",\n \"dimension\": 1536\n }\n })\n\n\nclass Tool(BaseModel):\n \"\"\"Tool metadata.\"\"\"\n name: str = Field(description=\"Unique name of the tool\")\n description: str = Field(description=\"Human-readable description of what the tool does\")\n enabled: bool = Field(default=True, description=\"Whether the tool is currently available for use\")\n\n\nclass ProjectToolUpdate(BaseModel):\n \"\"\"Update an agent-created tool's content.\"\"\"\n description: Optional[str] = Field(default=None, max_length=2000)\n parameters: Optional[str] = Field(default=None, max_length=10000)\n code: Optional[str] = Field(default=None, max_length=10240)\n\n\nclass LLMUpdate(BaseModel):\n \"\"\"Update an existing LLM configuration.\"\"\"\n class_name: Union[str, None] = Field(default=None, description=\"LLM implementation class name\")\n options: Union[str, Dict[str, Any], None] = Field(default=None, description=\"Provider-specific configuration options\")\n privacy: Optional[Literal[\"public\", \"private\"]] = Field(default=None, description=\"Privacy level: 'public' or 'private'\")\n description: Union[str, None] = Field(default=None, max_length=1000, description=\"Human-readable description of the LLM\")\n input_cost: Union[float, None] = Field(default=None, ge=0, description=\"Cost per 1M input tokens\")\n output_cost: Union[float, None] = Field(default=None, ge=0, description=\"Cost per 1M output tokens\")\n context_window: Union[int, None] = Field(default=None, ge=1, le=10000000, description=\"Maximum context window size in tokens\")\n\n @field_validator('class_name')\n @classmethod\n def class_name_must_be_valid(cls, v):\n if v is not None and v not in VALID_LLM_CLASSES:\n raise ValueError(f\"Invalid LLM class: '{v}'. Must be one of: {', '.join(sorted(VALID_LLM_CLASSES))}\")\n return v\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"options\": \"{\\\"model\\\": \\\"openai/gpt-4o-mini\\\"}\",\n \"privacy\": \"public\",\n \"description\": \"Updated model description\",\n \"input_cost\": 3.0,\n \"output_cost\": 12.0\n }\n })\n\n @field_validator('options', mode='before')\n @classmethod\n def serialize_options(cls, v):\n if isinstance(v, dict):\n return json.dumps(v)\n return v\n\n\nclass EmbeddingUpdate(BaseModel):\n \"\"\"Update an existing embedding model configuration.\"\"\"\n class_name: Union[str, None] = Field(default=None, description=\"Embedding implementation class name\")\n options: Union[str, Dict[str, Any], None] = Field(default=None, description=\"Provider-specific configuration options\")\n privacy: Optional[Literal[\"public\", \"private\"]] = Field(default=None, description=\"Privacy level: 'public' or 'private'\")\n description: Union[str, None] = Field(default=None, max_length=1000, description=\"Human-readable description of the embedding model\")\n dimension: Union[int, None] = Field(default=None, ge=1, le=65536, description=\"Output embedding dimension size\")\n\n @field_validator('class_name')\n @classmethod\n def class_name_must_be_valid(cls, v):\n if v is not None and v not in VALID_EMBEDDING_CLASSES:\n raise ValueError(f\"Invalid embedding class: '{v}'. Must be one of: {', '.join(sorted(VALID_EMBEDDING_CLASSES))}\")\n return v\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"options\": \"{\\\"model\\\": \\\"text-embedding-3-large\\\"}\",\n \"privacy\": \"public\",\n \"dimension\": 3072\n }\n })\n\n @field_validator('options', mode='before')\n @classmethod\n def serialize_options(cls, v):\n if isinstance(v, dict):\n return json.dumps(v)\n return v\n\n\nclass ImageGeneratorModel(BaseModel):\n \"\"\"Image generator registry entry — local worker or external provider.\"\"\"\n id: Union[int, None] = Field(default=None, description=\"Unique image generator identifier\")\n name: str = Field(description=\"Unique name identifier (used in /image/{name}/generate)\")\n class_name: str = Field(description=\"Provider: 'local' (worker module), 'openai' (incl. OpenAI-compat via options.base_url), 'google'\")\n options: Dict[str, Any] = Field(default_factory=dict, description=\"Provider-specific options (model, api_key, base_url, ...)\")\n privacy: Literal[\"public\", \"private\"] = Field(default=\"public\", description=\"Privacy level: 'public' (cloud-hosted) or 'private' (self-hosted)\")\n description: Union[str, None] = Field(default=None, max_length=1000, description=\"Human-readable description\")\n enabled: bool = Field(default=True, description=\"Whether this generator is currently usable\")\n teams: list[\"TeamModel\"] = Field(default=[], description=\"Teams that have access to this generator\")\n\n @field_validator('name')\n @classmethod\n def name_must_be_safe(cls, v):\n return validate_safe_name(v, \"Image generator name\")\n\n @field_validator('class_name')\n @classmethod\n def class_name_must_be_valid(cls, v):\n if v not in VALID_IMAGE_GENERATOR_CLASSES:\n raise ValueError(\n f\"Invalid image generator class: '{v}'. Must be one of: {', '.join(sorted(VALID_IMAGE_GENERATOR_CLASSES))}\"\n )\n return v\n\n @field_validator('options', mode='before')\n @classmethod\n def parse_options(cls, v):\n if isinstance(v, str):\n try:\n v = json.loads(v)\n except json.JSONDecodeError:\n return {}\n if isinstance(v, dict):\n try:\n # Image generators reuse LLM_SENSITIVE_KEYS for the same\n # `api_key`/`secret`/`password` bag — same encryption helper.\n from restai.utils.crypto import decrypt_sensitive_options, LLM_SENSITIVE_KEYS\n v = decrypt_sensitive_options(v, LLM_SENSITIVE_KEYS)\n except Exception:\n pass\n return v\n\n model_config = ConfigDict(from_attributes=True, json_schema_extra={\n \"example\": {\n \"name\": \"gpt-image-1.5\",\n \"class_name\": \"openai\",\n \"options\": {\"model\": \"gpt-image-1.5\", \"api_key\": \"sk-...\"},\n \"privacy\": \"public\",\n \"description\": \"OpenAI gpt-image-1.5 image model\",\n }\n })\n\n\nclass ImageGeneratorModelCreate(BaseModel):\n \"\"\"Create a new image generator entry.\"\"\"\n name: str = Field(description=\"Unique name identifier\")\n class_name: str = Field(description=\"Provider: 'local', 'openai', or 'google'\")\n options: Union[str, Dict[str, Any]] = Field(default_factory=dict, description=\"Provider-specific options\")\n privacy: Literal[\"public\", \"private\"] = Field(default=\"public\")\n description: Union[str, None] = Field(default=None, max_length=1000)\n enabled: bool = Field(default=True)\n\n @field_validator('name')\n @classmethod\n def name_must_be_safe(cls, v):\n return validate_safe_name(v, \"Image generator name\")\n\n @field_validator('class_name')\n @classmethod\n def class_name_must_be_valid(cls, v):\n if v not in VALID_IMAGE_GENERATOR_CLASSES:\n raise ValueError(\n f\"Invalid image generator class: '{v}'. Must be one of: {', '.join(sorted(VALID_IMAGE_GENERATOR_CLASSES))}\"\n )\n return v\n\n\nclass ImageGeneratorModelUpdate(BaseModel):\n \"\"\"Update an existing image generator entry. All fields optional.\"\"\"\n class_name: Union[str, None] = Field(default=None)\n options: Union[str, Dict[str, Any], None] = Field(default=None)\n privacy: Optional[Literal[\"public\", \"private\"]] = Field(default=None)\n description: Union[str, None] = Field(default=None, max_length=1000)\n enabled: Union[bool, None] = Field(default=None)\n\n @field_validator('class_name')\n @classmethod\n def class_name_must_be_valid(cls, v):\n if v is not None and v not in VALID_IMAGE_GENERATOR_CLASSES:\n raise ValueError(\n f\"Invalid image generator class: '{v}'. Must be one of: {', '.join(sorted(VALID_IMAGE_GENERATOR_CLASSES))}\"\n )\n return v\n\n @field_validator('options', mode='before')\n @classmethod\n def serialize_options(cls, v):\n if isinstance(v, dict):\n return json.dumps(v)\n return v\n\n\nclass SpeechToTextModel(BaseModel):\n \"\"\"Speech-to-text registry entry — local worker or external STT provider.\"\"\"\n id: Union[int, None] = Field(default=None, description=\"Unique STT model identifier\")\n name: str = Field(description=\"Unique name (used in /audio/{name}/transcript)\")\n class_name: str = Field(description=\"Provider: 'local', 'openai', 'google', 'deepgram', 'assemblyai'\")\n options: Dict[str, Any] = Field(default_factory=dict, description=\"Provider-specific options (model, api_key, base_url, ...)\")\n privacy: Literal[\"public\", \"private\"] = Field(default=\"public\", description=\"Privacy level\")\n description: Union[str, None] = Field(default=None, max_length=1000, description=\"Human-readable description\")\n enabled: bool = Field(default=True, description=\"Whether this model is currently usable\")\n teams: list[\"TeamModel\"] = Field(default=[], description=\"Teams that have access to this model\")\n\n @field_validator('name')\n @classmethod\n def name_must_be_safe(cls, v):\n return validate_safe_name(v, \"Speech-to-text model name\")\n\n @field_validator('class_name')\n @classmethod\n def class_name_must_be_valid(cls, v):\n if v not in VALID_SPEECH_TO_TEXT_CLASSES:\n raise ValueError(\n f\"Invalid speech-to-text class: '{v}'. Must be one of: {', '.join(sorted(VALID_SPEECH_TO_TEXT_CLASSES))}\"\n )\n return v\n\n @field_validator('options', mode='before')\n @classmethod\n def parse_options(cls, v):\n if isinstance(v, str):\n try:\n v = json.loads(v)\n except json.JSONDecodeError:\n return {}\n if isinstance(v, dict):\n try:\n from restai.utils.crypto import decrypt_sensitive_options, LLM_SENSITIVE_KEYS\n v = decrypt_sensitive_options(v, LLM_SENSITIVE_KEYS)\n except Exception:\n pass\n return v\n\n model_config = ConfigDict(from_attributes=True, json_schema_extra={\n \"example\": {\n \"name\": \"whisper-1\",\n \"class_name\": \"openai\",\n \"options\": {\"model\": \"whisper-1\", \"api_key\": \"sk-...\"},\n \"privacy\": \"public\",\n \"description\": \"OpenAI Whisper-1 hosted transcription\",\n }\n })\n\n\nclass SpeechToTextModelCreate(BaseModel):\n \"\"\"Create a new speech-to-text registry entry.\"\"\"\n name: str = Field(description=\"Unique name identifier\")\n class_name: str = Field(description=\"Provider: 'local', 'openai', 'google', 'deepgram', 'assemblyai'\")\n options: Union[str, Dict[str, Any]] = Field(default_factory=dict)\n privacy: Literal[\"public\", \"private\"] = Field(default=\"public\")\n description: Union[str, None] = Field(default=None, max_length=1000)\n enabled: bool = Field(default=True)\n\n @field_validator('name')\n @classmethod\n def name_must_be_safe(cls, v):\n return validate_safe_name(v, \"Speech-to-text model name\")\n\n @field_validator('class_name')\n @classmethod\n def class_name_must_be_valid(cls, v):\n if v not in VALID_SPEECH_TO_TEXT_CLASSES:\n raise ValueError(\n f\"Invalid speech-to-text class: '{v}'. Must be one of: {', '.join(sorted(VALID_SPEECH_TO_TEXT_CLASSES))}\"\n )\n return v\n\n\nclass SpeechToTextModelUpdate(BaseModel):\n \"\"\"Update an existing speech-to-text registry entry.\"\"\"\n class_name: Union[str, None] = Field(default=None)\n options: Union[str, Dict[str, Any], None] = Field(default=None)\n privacy: Optional[Literal[\"public\", \"private\"]] = Field(default=None)\n description: Union[str, None] = Field(default=None, max_length=1000)\n enabled: Union[bool, None] = Field(default=None)\n\n @field_validator('class_name')\n @classmethod\n def class_name_must_be_valid(cls, v):\n if v is not None and v not in VALID_SPEECH_TO_TEXT_CLASSES:\n raise ValueError(\n f\"Invalid speech-to-text class: '{v}'. Must be one of: {', '.join(sorted(VALID_SPEECH_TO_TEXT_CLASSES))}\"\n )\n return v\n\n @field_validator('options', mode='before')\n @classmethod\n def serialize_options(cls, v):\n if isinstance(v, dict):\n return json.dumps(v)\n return v\n\n\nclass ProjectSecretModel(BaseModel):\n \"\"\"A project-scoped secret used by agentic tools (e.g. browser_fill\n via `secret_ref`). Values are masked on read — the plaintext never\n leaves the server.\"\"\"\n id: Union[int, None] = Field(default=None)\n project_id: int = Field(description=\"Owning project id\")\n name: str = Field(description=\"Unique (within the project) reference name — e.g. 'portal_password'\")\n value: str = Field(description=\"Masked as '********' in API responses\")\n description: Union[str, None] = Field(default=None, max_length=500)\n\n @field_validator('name')\n @classmethod\n def name_must_be_safe(cls, v):\n return validate_safe_name(v, \"Secret name\")\n\n model_config = ConfigDict(from_attributes=True)\n\n\nclass ProjectSecretCreate(BaseModel):\n \"\"\"Create a new project secret.\"\"\"\n name: str = Field(description=\"Unique (within the project) reference name\")\n value: str = Field(min_length=1, max_length=10000, description=\"Plaintext value — encrypted at rest before DB write\")\n description: Union[str, None] = Field(default=None, max_length=500)\n\n @field_validator('name')\n @classmethod\n def name_must_be_safe(cls, v):\n return validate_safe_name(v, \"Secret name\")\n\n\nclass ProjectSecretUpdate(BaseModel):\n \"\"\"Patch a project secret. Value `\"********\"` preserves the existing\n stored value (the same mask-round-trip pattern LLMs and image\n generators use).\"\"\"\n value: Union[str, None] = Field(default=None, max_length=10000)\n description: Union[str, None] = Field(default=None, max_length=500)\n\n\nclass UserProject(BaseModel):\n \"\"\"Project reference for a user.\"\"\"\n id: int = Field(description=\"Unique project identifier\")\n model_config = ConfigDict(from_attributes=True)\n\n\nclass ProjectUser(BaseModel):\n \"\"\"User reference for a project.\"\"\"\n username: str = Field(description=\"Username of the assigned user\")\n model_config = ConfigDict(from_attributes=True)\n\n\nclass MCPServer(BaseModel):\n \"\"\"MCP server connection configuration.\"\"\"\n host: str = Field(description=\"MCP server command or URL (e.g. 'npx', 'uvx', or an HTTP endpoint)\")\n args: Union[list[str], None] = Field(default=None, description=\"Command-line arguments for the MCP server process\")\n env: Union[dict[str, str], None] = Field(default=None, description=\"Environment variables to pass to the MCP server process\")\n headers: Union[dict[str, str], None] = Field(default=None, description=\"HTTP headers for authentication (e.g. {'Authorization': 'Bearer token'})\")\n tools: Union[str, None] = Field(default=None, description=\"Comma-separated list of specific tools to enable from this server\")\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"host\": \"npx\",\n \"args\": [\"-y\", \"@modelcontextprotocol/server-filesystem\", \"/tmp\"],\n \"env\": {},\n \"tools\": None\n }\n })\n\n\nclass MCPProbeRequest(BaseModel):\n \"\"\"Probe an MCP server to discover available tools.\"\"\"\n host: str = Field(description=\"MCP server command or URL to probe\")\n args: Union[list[str], None] = Field(default=None, description=\"Command-line arguments for the MCP server process\")\n env: Union[dict[str, str], None] = Field(default=None, description=\"Environment variables to pass to the MCP server process\")\n headers: Union[dict[str, str], None] = Field(default=None, description=\"HTTP headers for authentication\")\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"host\": \"npx\",\n \"args\": [\"-y\", \"@modelcontextprotocol/server-filesystem\", \"/tmp\"]\n }\n })\n\n\nclass SyncSource(BaseModel):\n \"\"\"External source configuration for knowledge base auto-sync.\"\"\"\n type: Literal[\"url\", \"s3\", \"confluence\", \"sharepoint\", \"gdrive\"] = Field(description=\"Source type: 'url', 's3', 'confluence', 'sharepoint', or 'gdrive'\")\n name: str = Field(max_length=200, description=\"User-friendly label for this source\")\n # URL source\n url: Union[str, None] = Field(default=None, max_length=2000, description=\"Web URL to sync\")\n # S3 source\n s3_bucket: Union[str, None] = Field(default=None, max_length=200, description=\"S3 bucket name\")\n s3_prefix: Union[str, None] = Field(default=None, max_length=500, description=\"S3 key prefix filter\")\n s3_region: Union[str, None] = Field(default=None, max_length=50, description=\"AWS region\")\n s3_access_key: Union[str, None] = Field(default=None, max_length=200, description=\"AWS access key ID\")\n s3_secret_key: Union[str, None] = Field(default=None, max_length=200, description=\"AWS secret access key\")\n # Confluence source\n confluence_base_url: Union[str, None] = Field(default=None, max_length=2000, description=\"Confluence Cloud base URL (e.g. https://yoursite.atlassian.net)\")\n confluence_space_key: Union[str, None] = Field(default=None, max_length=50, description=\"Confluence space key\")\n confluence_email: Union[str, None] = Field(default=None, max_length=255, description=\"Confluence API user email\")\n confluence_api_token: Union[str, None] = Field(default=None, max_length=1000, description=\"Confluence API token\")\n # SharePoint / Microsoft 365 source\n sharepoint_tenant_id: Union[str, None] = Field(default=None, max_length=100, description=\"Azure AD tenant ID\")\n sharepoint_client_id: Union[str, None] = Field(default=None, max_length=100, description=\"Azure AD app client ID\")\n sharepoint_client_secret: Union[str, None] = Field(default=None, max_length=500, description=\"Azure AD app client secret\")\n sharepoint_site_name: Union[str, None] = Field(default=None, max_length=200, description=\"SharePoint site name (e.g. 'MySite' from yourorg.sharepoint.com/sites/MySite)\")\n sharepoint_folder: Union[str, None] = Field(default=None, max_length=500, description=\"Folder path filter within the document library (e.g. 'General/Docs')\")\n # Google Drive source\n gdrive_folder_id: Union[str, None] = Field(default=None, max_length=200, description=\"Google Drive folder ID to sync\")\n gdrive_service_account_json: Union[str, None] = Field(default=None, max_length=10000, description=\"Google service account JSON key (paste full JSON)\")\n # Ingestion options\n splitter: Literal[\"sentence\", \"token\"] = Field(default=\"sentence\", description=\"Text splitting strategy\")\n chunks: int = Field(default=512, ge=32, le=8192, description=\"Chunk size\")\n # Per-source scheduling\n sync_interval: int = Field(default=60, ge=5, le=10080, description=\"Sync interval in minutes for this source\")\n last_sync: Union[str, None] = Field(default=None, description=\"Timestamp of last successful sync for this source\")\n model_config = ConfigDict(from_attributes=True)\n\n\nclass ProjectOptions(BaseModel):\n \"\"\"Project-level configuration options.\"\"\"\n logging: bool = Field(default=True, description=\"Enable inference logging for this project\")\n redact_inference_logs: bool = Field(default=False, description=\"Redact secrets (API keys, tokens, credentials) from inference logs before persisting\")\n llm_rerank: Union[bool, None] = Field(default=None, description=\"Enable LLM-based reranking of retrieved documents\")\n rerank_llm: Union[str, None] = Field(default=None, max_length=255, description=\"LLM used for LLM reranking (RAG only). Empty/None = use the project's own LLM.\")\n tables: Union[str, None] = Field(default=None, description=\"Comma-separated list of allowed database tables for natural language to SQL queries\")\n tools: Union[str, None] = Field(default=None, description=\"Comma-separated list of enabled tool names\")\n score: float = Field(default=0.0, description=\"Minimum similarity score threshold for retrieved documents\")\n k: int = Field(default=4, ge=0, le=100, description=\"Number of documents to retrieve from the knowledge base\")\n max_iterations: int = Field(default=10, ge=1, le=100, description=\"Maximum iterations for agent project execution\")\n auto_plan: bool = Field(default=False, description=\"Plan-and-execute: before the first turn, run a one-shot planner LLM call. If the request is multi-step, the agent runs each subtask as its own bounded iteration loop (with shared session) and synthesizes a final answer at the end. Avoids hitting max_iterations on big tasks like 'audit this repo'.\")\n connection: Union[str, None] = Field(default=None, max_length=2000, description=\"Database connection string for natural language to SQL queries\")\n mcp_servers: Union[list[MCPServer], None] = Field(default=None, description=\"List of MCP server configurations for agent projects\")\n telegram_token: Union[str, None] = Field(default=None, description=\"Telegram bot token for Telegram integration\")\n telegram_default_chat_id: Union[int, None] = Field(default=None, description=\"Default Telegram chat ID for the send_telegram builtin tool. Set to a chat that has already messaged the bot (Telegram bots cannot initiate). To find the value, message the bot with `/chatid` after enabling the integration.\")\n telegram_allowed_chat_ids: Union[str, None] = Field(default=None, description=\"Comma-separated allowlist of Telegram chat IDs that may talk to the bot (DMs and groups). Empty = anyone who finds the bot can chat with it. The /chatid command always works regardless so unauthorized users can self-discover their id and ask the admin to add them.\")\n slack_bot_token: Union[str, None] = Field(default=None, description=\"Slack bot token (xoxb-...) for Slack integration\")\n # WhatsApp Business Cloud API (Meta first-party, no third-party broker).\n whatsapp_phone_number_id: Union[str, None] = Field(default=None, description=\"Meta-assigned phone number id (the long numeric value visible in Meta Business Suite under WhatsApp → API Setup). The webhook routes inbound messages to this project by this id.\")\n whatsapp_access_token: Union[str, None] = Field(default=None, description=\"WhatsApp Business Cloud API access token (long-lived System User token recommended). Encrypted at rest.\")\n whatsapp_app_secret: Union[str, None] = Field(default=None, description=\"Meta App secret used to verify the X-Hub-Signature-256 header on inbound webhooks. Encrypted at rest.\")\n whatsapp_verify_token: Union[str, None] = Field(default=None, description=\"Admin-chosen string Meta echoes back during the initial GET subscription handshake. Encrypted at rest.\")\n whatsapp_default_to: Union[str, None] = Field(default=None, description=\"Default recipient phone number (E.164, no '+') for the send_whatsapp builtin tool — typically your own phone for proactive notifications. Outbound is constrained to WhatsApp's 24h customer-service window unless using a pre-approved template (template messages are not supported by this tool).\")\n whatsapp_allowed_phone_numbers: Union[str, None] = Field(default=None, description=\"Comma-separated allowlist of E.164 senders (no '+'). Empty = anyone with the bot's number can chat. Recommended for production — protects WhatsApp number quality rating from spam.\")\n # NOTE: per-project SMTP options were removed — the send_email\n # builtin now resolves credentials from the project's owning team\n # (Settings → Integrations → Email) and falls back to the platform\n # Notifications settings. See restai/utils/email.py.\n # Outbound SMS via Twilio's REST API. Pick Twilio specifically because\n # the auth model is a single account-sid + auth-token pair, no OAuth\n # dance, no webhook subscription. Other providers can be added later.\n twilio_account_sid: Union[str, None] = Field(default=None, description=\"Twilio Account SID (starts with 'AC...').\")\n twilio_auth_token: Union[str, None] = Field(default=None, description=\"Twilio Auth Token. Encrypted at rest.\")\n twilio_from_number: Union[str, None] = Field(default=None, description=\"Twilio sender phone number in E.164 format (e.g. +15551234567). Must be a number you've provisioned in Twilio.\")\n sms_default_to: Union[str, None] = Field(default=None, description=\"Default recipient phone number (E.164) for the send_sms builtin tool.\")\n # Outbound event webhooks. Lets external systems (Zapier, n8n, custom CRM)\n # react to project events without scraping the audit log.\n webhook_url: Union[str, None] = Field(default=None, description=\"HTTPS endpoint that receives event POSTs from this project. Empty = webhooks disabled.\")\n webhook_secret: Union[str, None] = Field(default=None, description=\"Shared secret used to sign webhook payloads (HMAC-SHA256, sent in X-RESTai-Signature header). Encrypted at rest.\")\n webhook_events: Union[str, None] = Field(default=None, description=\"Comma-separated event types this project subscribes to. Supported: budget_exceeded, sync_completed, eval_completed, routine_failed. Empty = all events.\")\n # Content moderation (used by the moderate_content builtin tool).\n moderation_blocklist: Union[str, None] = Field(default=None, description=\"Comma-separated terms that the moderate_content tool flags (and redacts when moderation_redact_pii is on). Case-insensitive substring match — keep literal, no regex.\")\n moderation_redact_pii: Union[bool, None] = Field(default=True, description=\"When true, moderate_content returns a SANITIZED copy of the input with PII/blocklist matches replaced by [REDACTED:<type>]. Default on.\")\n blockly_workspace: Union[dict, None] = Field(default=None, description=\"Blockly workspace JSON for block projects\")\n rate_limit: Union[int, None] = Field(default=None, ge=1, le=10000, description=\"Maximum requests per minute (None = unlimited)\")\n budget: Union[float, None] = Field(default=None, ge=0, description=\"Monthly cost budget (currency) for this project (None = unlimited). Part of the unified cost-budget model.\")\n guard_output: Union[str, None] = Field(default=None, description=\"Id of the guard project for output checking (stored as the project id, not the name)\")\n guard_mode: Union[str, None] = Field(default=\"block\", description=\"Guard behavior: 'block' or 'warn'\")\n eval_llm: Union[str, None] = Field(default=None, max_length=255, description=\"LLM used to judge evaluation runs. Empty/None = use the project's own LLM.\")\n search_knowledge_project: Union[str, None] = Field(default=None, max_length=255, description=\"Name of the RAG project the search_knowledge builtin queries (agent projects). Empty = disabled.\")\n sync_sources: Union[list[SyncSource], None] = Field(default=None, description=\"External sources for knowledge base auto-sync\")\n sync_enabled: Union[bool, None] = Field(default=None, description=\"Enable automatic knowledge base sync\")\n enable_knowledge_graph: Union[bool, None] = Field(default=None, description=\"Enable entity extraction and knowledge graph features (RAG projects only)\")\n ner_model: Union[str, None] = Field(default=None, description=\"HuggingFace NER model name (default: dslim/bert-base-NER)\")\n agent_mode: Union[Literal[\"auto\", \"function_calling\", \"react\"], None] = Field(\n default=None,\n description=\"Agent execution mode (agent projects only): 'auto' tries native function calling and falls back to text-based ReAct on first-turn error; 'function_calling' always uses native (errors propagate); 'react' always uses text-based ReAct prompting.\"\n )\n agent_loop: Union[Literal[\"restai\", \"claude\", \"llamaindex\", \"smolagents\", \"openai_agents\"], None] = Field(\n default=None,\n description=\"Agent loop implementation (agent projects only): 'restai' (default, our agent2 runtime — works with any platform LLM); 'claude' (Claude Agent SDK — requires an Anthropic-class LLM, talks directly to Anthropic); 'llamaindex' (upstream llama-index AgentWorkflow — FunctionAgent / ReActAgent over the project's llama-index LLM, works with any platform LLM); 'smolagents' (HuggingFace smolagents ToolCallingAgent — uses OpenAI-compatible endpoints natively for OpenAI / Ollama / vLLM / Grok / Azure; LiteLLM fallback only for Anthropic / Gemini / Bedrock); 'openai_agents' (OpenAI Agents SDK — requires an OpenAI-class LLM, talks directly to OpenAI). More options will be added over time.\"\n )\n memory_bank_enabled: bool = Field(\n default=False,\n description=\"Agent projects only. When enabled, all conversations in this project contribute to a shared 'memory bank' that is injected into the system prompt of every chat. Compressed dynamically (per-conversation → per-day → per-week → per-month) to fit within memory_bank_max_tokens. Disabled by default; surfaces a privacy disclaimer in the UI because every project member sees summaries derived from every other member's conversations.\"\n )\n memory_bank_max_tokens: int = Field(\n default=2000,\n ge=200,\n le=10000,\n description=\"Token budget for the rendered memory bank block. Older entries get rolled up into coarser time buckets when this is exceeded.\"\n )\n memory_search_enabled: bool = Field(\n default=False,\n description=\"Agent projects only. When enabled, every conversation turn (OutputDatabase row) is indexed into a per-project ChromaDB collection so the `search_memories` builtin tool can semantically retrieve past Q/A from anywhere in this project's history. Independent of `memory_bank_enabled` — the bank broadcasts a fixed slice into every system prompt, while this powers on-demand retrieval. Requires the project to have an embedding configured.\"\n )\n # Agentic Browser — the browser_* builtin tools.\n browser_allowed_domains: Union[str, None] = Field(\n default=None,\n description=\"Comma-separated allowlist of domains the agent's headless Chromium may visit (e.g. 'acme.com, supplier.net'). Empty = unrestricted (not recommended). Matches full hostnames or suffix-globs like '*.acme.com'. browser_goto refuses anything not on the list.\"\n )\n browser_allow_eval: bool = Field(\n default=False,\n description=\"When true, the `browser_eval` tool is available for this project — lets the agent execute arbitrary JavaScript in the page context. Powerful but dangerous (a prompt-injected page can execute anything the domain permits). Off by default; admin must explicitly opt in.\"\n )\n model_config = ConfigDict(from_attributes=True)\n\n\nclass ProjectBaseModel(BaseModel):\n \"\"\"Base project model with all properties.\"\"\"\n id: int = Field(description=\"Unique project identifier\")\n name: str = Field(description=\"URL-friendly project name (used in API paths)\")\n embeddings: Union[str, None] = Field(default=None, description=\"Name of the embedding model used for this project\")\n llm: Union[str, None] = Field(default=None, description=\"Name of the LLM used for this project\")\n type: str = Field(description=\"Project type: 'rag', 'agent', or 'block'\")\n system: Union[str, None] = Field(default=None, description=\"System prompt for the LLM\")\n censorship: Union[str, None] = Field(default=None, description=\"Censorship message returned when the guard rejects a query\")\n vectorstore: Union[str, None] = Field(default=None, description=\"Vector store backend: 'chroma' or 'redis'\")\n guard: Union[str, None] = Field(default=None, description=\"Id of the guard project used for input checking (stored as the project id, not the name)\")\n guard_name: Union[str, None] = Field(default=None, description=\"Read-only: display name of the input-guard project, resolved from `guard`\")\n guard_output_name: Union[str, None] = Field(default=None, description=\"Read-only: display name of the output-guard project, resolved from options.guard_output\")\n human_name: Union[str, None] = Field(default=None, description=\"Human-readable display name for the project\")\n human_description: Union[str, None] = Field(default=None, description=\"Human-readable description of the project\")\n public: bool = Field(default=False, description=\"Whether the project is publicly accessible without authentication\")\n creator: Union[int, None] = Field(default=None, description=\"User ID of the project creator\")\n creator_username: Union[str, None] = Field(default=None, description=\"Username of the project creator\")\n default_prompt: Union[str, None] = Field(default=None, description=\"Default prompt template for the project\")\n options: Union[str, ProjectOptions] = Field(default=ProjectOptions(), description=\"Project configuration options\")\n users: list[ProjectUser] = Field(default=[], description=\"Users assigned to this project\")\n model_config = ConfigDict(from_attributes=True)\n\n @field_validator('options', mode='before')\n @classmethod\n def parse_options(cls, v):\n if isinstance(v, str):\n try:\n v = json.loads(v)\n except json.JSONDecodeError:\n return ProjectOptions()\n if isinstance(v, dict):\n try:\n from restai.utils.crypto import decrypt_sensitive_options, PROJECT_SENSITIVE_KEYS\n v = decrypt_sensitive_options(v, PROJECT_SENSITIVE_KEYS)\n except Exception:\n pass\n return ProjectOptions(**v)\n return v\n\n\n\nclass ProjectModel(ProjectBaseModel):\n \"\"\"Project model with team information.\"\"\"\n team: Union[\"TeamModel\", None] = Field(default=None, description=\"Team that owns this project\")\n model_config = ConfigDict(from_attributes=True)\n\n\nclass ProjectModelCreate(BaseModel):\n \"\"\"Create a new AI project.\"\"\"\n name: str = Field(description=\"URL-friendly project name (must be unique)\")\n embeddings: Union[str, None] = Field(default=None, description=\"Name of the embedding model (required for RAG projects)\")\n llm: Union[str, None] = Field(default=None, description=\"Name of the LLM to use (not required for block projects)\")\n type: Literal[\"rag\", \"agent\", \"block\"] = Field(description=\"Project type: 'rag', 'agent', or 'block'\")\n human_name: Union[str, None] = Field(default=None, max_length=200, description=\"Human-readable display name\")\n human_description: Union[str, None] = Field(default=None, max_length=2000, description=\"Human-readable project description\")\n vectorstore: Union[str, None] = Field(default=None, description=\"Vector store backend: 'chroma' or 'redis'\")\n team_id: int = Field(description=\"ID of the team that will own this project\")\n\n @field_validator('name')\n @classmethod\n def name_must_be_safe(cls, v):\n return validate_safe_name(v, \"Project name\")\n\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"name\": \"my-rag-project\",\n \"embeddings\": \"text-embedding-3-small\",\n \"llm\": \"gpt-4o\",\n \"type\": \"rag\",\n \"human_name\": \"My RAG Project\",\n \"human_description\": \"A knowledge base powered by GPT-4o\",\n \"vectorstore\": \"chroma\",\n \"team_id\": 1\n }\n })\n\n\nclass ProjectResponse(ProjectBaseModel):\n \"\"\"Project response with simplified team.\"\"\"\n team: Union[\"TeamResponse\", None] = Field(default=None, description=\"Simplified team reference\")\n\n\nclass ProjectsResponse(BaseModel):\n \"\"\"Paginated list of projects.\"\"\"\n projects: list[ProjectResponse] = Field(description=\"List of projects in the current page\")\n total: int = Field(description=\"Total number of projects matching the query\")\n start: int = Field(description=\"Starting index of the current page (0-based)\")\n end: int = Field(description=\"Ending index of the current page (exclusive)\")\n\n\nclass ProjectInfo(ProjectModel):\n \"\"\"Extended project info including chunk count and LLM details.\"\"\"\n chunks: int = Field(default=0, description=\"Number of document chunks in the project's knowledge base\")\n llm_privacy: Union[str, None] = Field(default=None, description=\"Privacy level of the associated LLM\")\n\n\nclass UserOptions(BaseModel):\n \"\"\"User-level configuration options.\"\"\"\n preferred_team_id: Union[int, None] = Field(default=None, description=\"ID of the team whose branding to use\")\n # UI language preference. Stored per-user inside the options JSON\n # blob so it follows the user across devices. Frontend falls back\n # to localStorage when the user is unauthenticated (login screen)\n # or the field is unset. Valid values: \"en\", \"pt-PT\", \"zh-CN\".\n language: Union[str, None] = Field(default=None, max_length=10, description=\"UI language code (e.g. 'en', 'pt-PT', 'zh-CN'). Null = use browser / default.\")\n model_config = ConfigDict(from_attributes=True)\n\n\nclass TOTPSetupResponse(BaseModel):\n \"\"\"TOTP setup response with secret and recovery codes (shown once).\"\"\"\n secret: str = Field(description=\"Base32 TOTP secret for manual entry\")\n provisioning_uri: str = Field(description=\"otpauth:// URI for QR code generation\")\n recovery_codes: list[str] = Field(description=\"One-time recovery codes (store securely)\")\n\nclass TOTPVerifyRequest(BaseModel):\n \"\"\"Verify TOTP code to complete login.\"\"\"\n token: str = Field(description=\"Temporary JWT token from login step\")\n code: str = Field(max_length=20, description=\"6-digit TOTP code or recovery code\")\n\nclass TOTPEnableRequest(BaseModel):\n \"\"\"Confirm TOTP setup with a valid code and password.\"\"\"\n code: str = Field(max_length=6, description=\"6-digit TOTP code from authenticator app\")\n password: str = Field(description=\"Current password for confirmation\")\n\nclass TOTPDisableRequest(BaseModel):\n \"\"\"Disable TOTP (requires password confirmation).\"\"\"\n password: str = Field(description=\"Current password for confirmation\")\n\n\nclass TOTPSetupRequest(BaseModel):\n \"\"\"Step-up auth required to (re)generate a TOTP secret.\n\n Always requires `password` so a session-only attacker can't\n silently rotate the second factor. If 2FA is currently enabled,\n `code` is also required and must verify against the EXISTING\n secret before that secret is overwritten — otherwise the attacker\n could lock the legitimate user out by replacing the seed.\n \"\"\"\n password: str = Field(description=\"Current password for confirmation\")\n code: Optional[str] = Field(\n default=None,\n max_length=20,\n description=\"Current TOTP code (or recovery code), required only when 2FA is already enabled\",\n )\n\n\nclass ApiKeyCreate(BaseModel):\n \"\"\"Create a new API key.\"\"\"\n description: str = Field(default=\"\", description=\"Human-readable description of the API key's purpose\")\n team_id: int = Field(description=\"Team that owns this key; its budget is billed for direct-access usage. Must be a team the key owner belongs to.\")\n allowed_projects: Union[list[int], None] = Field(default=None, description=\"List of project IDs this key can access. Null means all projects.\")\n read_only: bool = Field(default=False, description=\"If true, the key can only query but not modify projects.\")\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"description\": \"CI/CD pipeline key\",\n \"team_id\": 1,\n \"allowed_projects\": [1, 2],\n \"read_only\": True\n }\n })\n\n\nclass ApiKeyResponse(BaseModel):\n \"\"\"API key metadata (without the key value).\"\"\"\n id: int = Field(description=\"Unique API key identifier\")\n key_prefix: str = Field(description=\"First characters of the key for identification (e.g. 'sk-abc...')\")\n description: str = Field(description=\"Human-readable description of the API key\")\n created_at: datetime = Field(description=\"Timestamp when the API key was created\")\n team_id: Union[int, None] = Field(default=None, description=\"Team billed for this key's direct-access usage (null for legacy keys)\")\n allowed_projects: Union[list[int], None] = Field(default=None, description=\"Project IDs this key can access, null means all\")\n read_only: bool = Field(default=False, description=\"Whether this key is read-only\")\n token_quota_monthly: Union[int, None] = Field(default=None, description=\"Monthly token quota (null = unlimited)\")\n tokens_used_this_month: int = Field(default=0, description=\"Tokens consumed in the current quota window\")\n quota_reset_at: Union[datetime, None] = Field(default=None, description=\"Timestamp when the current quota window rolls over\")\n cost_budget_monthly: Union[float, None] = Field(default=None, description=\"Monthly cost budget (currency) for this key (null = unlimited)\")\n model_config = ConfigDict(from_attributes=True)\n\n @field_validator('allowed_projects', mode='before')\n @classmethod\n def parse_allowed_projects(cls, v):\n if isinstance(v, str):\n try:\n return json.loads(v)\n except (json.JSONDecodeError, TypeError):\n return None\n return v\n\n\nclass ApiKeyUpdate(BaseModel):\n \"\"\"Patch an API key's quota / metadata. Only fields that are set get\n applied — omitted fields are left untouched.\"\"\"\n description: Union[str, None] = Field(default=None, description=\"New description\")\n token_quota_monthly: Union[int, None] = Field(default=None, ge=0, description=\"New monthly token cap (0 or null = clear quota)\")\n cost_budget_monthly: Union[float, None] = Field(default=None, ge=0, description=\"New monthly cost budget (0 = clear). Omitted = unchanged.\")\n reset_usage: bool = Field(default=False, description=\"Zero tokens_used_this_month and push quota_reset_at to the 1st of next month\")\n\n\nclass ApiKeyCreatedResponse(BaseModel):\n \"\"\"API key creation response including the plaintext key (shown only once).\"\"\"\n id: int = Field(description=\"Unique API key identifier\")\n api_key: str = Field(description=\"The full plaintext API key (only shown once at creation time)\")\n key_prefix: str = Field(description=\"First characters of the key for identification\")\n description: str = Field(description=\"Human-readable description of the API key\")\n created_at: datetime = Field(description=\"Timestamp when the API key was created\")\n team_id: Union[int, None] = Field(default=None, description=\"Team billed for this key's direct-access usage\")\n allowed_projects: Union[list[int], None] = Field(default=None, description=\"Project IDs this key can access\")\n read_only: bool = Field(default=False, description=\"Whether this key is read-only\")\n\n\nclass User(BaseModel):\n \"\"\"Full user profile with projects, teams, and API keys.\"\"\"\n id: int = Field(description=\"Unique user identifier\")\n username: str = Field(description=\"Unique username\")\n is_admin: bool = Field(default=False, description=\"Whether the user has administrator privileges\")\n is_private: bool = Field(default=False, description=\"Whether the user's profile is private\")\n is_restricted: bool = Field(default=False, description=\"Whether the user is restricted (read-only, no project creation/editing)\")\n is_suspended: bool = Field(default=False, description=\"Whether the user is suspended (cannot log in; API keys rejected)\")\n projects: list[UserProject] = Field(default=[], description=\"Projects assigned to this user\")\n api_keys: list[ApiKeyResponse] = Field(default=[], description=\"API keys owned by this user\")\n level: Union[str, None] = Field(default=None, description=\"User's subscription or access level\")\n options: Union[str, UserOptions] = Field(default=UserOptions(), description=\"User configuration options\")\n teams: list[\"TeamModel\"] = Field(default=[], description=\"Teams the user is a member of\")\n admin_teams: list[\"TeamModel\"] = Field(default=[], description=\"Teams the user is an admin of\")\n # API key scope — set during auth, excluded from serialization\n api_key_allowed_projects: Union[list[int], None] = Field(default=None, exclude=True)\n api_key_read_only: bool = Field(default=False, exclude=True)\n # ID of the ApiKeyDatabase row that authenticated this request (None\n # for basic / cookie auth). Used by the monthly token-quota check\n # and the log_inference usage counter.\n api_key_id: Union[int, None] = Field(default=None, exclude=True)\n # Team the authenticating API key bills (None for basic/cookie auth\n # and legacy keys). Pins direct-access team attribution deterministically.\n api_key_team_id: Union[int, None] = Field(default=None, exclude=True)\n model_config = ConfigDict(from_attributes=True)\n\n @field_validator('options', mode='before')\n @classmethod\n def parse_options(cls, v):\n if isinstance(v, str):\n try:\n return UserOptions(**json.loads(v))\n except json.JSONDecodeError:\n return UserOptions()\n elif isinstance(v, dict):\n return UserOptions(**v)\n return v\n\n def has_project_access(self, project_id: int) -> bool:\n \"\"\"Check if the user can access a specific project.\"\"\"\n if self.is_admin:\n return True\n return any(p.id == project_id for p in self.projects)\n\n def get_project_ids(self) -> set[int]:\n \"\"\"Return the set of project IDs this user can access.\"\"\"\n return {p.id for p in self.projects}\n\n def has_api_key_project_access(self, project_id: int) -> bool:\n \"\"\"Check if the API key scope allows access to a specific project.\"\"\"\n if self.is_admin:\n return True\n if self.api_key_allowed_projects is None:\n return True\n return project_id in self.api_key_allowed_projects\n\n @property\n def is_read_only(self) -> bool:\n \"\"\"Check if the current auth context is read-only (via API key scope).\"\"\"\n if self.is_admin:\n return False\n return self.api_key_read_only\n\n\nclass LimitedUser(BaseModel):\n \"\"\"Limited user info visible to non-admin users.\"\"\"\n id: int = Field(description=\"Unique user identifier\")\n username: str = Field(description=\"Unique username\")\n model_config = ConfigDict(from_attributes=True)\n\n\nclass UsersResponse(BaseModel):\n \"\"\"List of users.\"\"\"\n users: list[Union[User, LimitedUser]] = Field(description=\"List of user objects (full or limited based on requester's role)\")\n\n\nclass UserBase(BaseModel):\n \"\"\"Base user model.\"\"\"\n username: str = Field(description=\"Unique username\")\n\n @field_validator('username')\n @classmethod\n def username_must_be_safe(cls, v):\n return validate_safe_name(v, \"Username\")\n\n\nclass UserLogin(BaseModel):\n \"\"\"Login credentials.\"\"\"\n user: str = Field(description=\"Username\")\n password: str = Field(description=\"User's password\")\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"user\": \"admin\",\n \"password\": \"your-password\"\n }\n })\n\n\nclass UserCreate(UserBase):\n \"\"\"Create a new user.\"\"\"\n password: str = Field(description=\"Password for the new user\")\n is_admin: bool = Field(default=False, description=\"Grant administrator privileges\")\n is_private: bool = Field(default=False, description=\"Make user profile private\")\n is_restricted: bool = Field(default=False, description=\"Restrict user to read-only operations\")\n is_suspended: bool = Field(default=False, description=\"Suspend the user (cannot log in; API keys rejected)\")\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"username\": \"johndoe\",\n \"password\": \"securepassword123\",\n \"is_admin\": False,\n \"is_private\": False\n }\n })\n\n\nclass UserUpdate(BaseModel):\n \"\"\"Update user properties.\"\"\"\n password: str = Field(default=None, description=\"New password for the user\")\n totp_code: Optional[str] = Field(default=None, max_length=6, description=\"TOTP code required when changing password with 2FA enabled\")\n is_admin: bool = Field(default=None, description=\"Update administrator privileges\")\n is_private: bool = Field(default=None, description=\"Update profile privacy setting\")\n is_restricted: Union[bool, None] = Field(default=None, description=\"Update restricted mode\")\n is_suspended: Union[bool, None] = Field(default=None, description=\"Suspend / unsuspend the user (cannot log in; API keys rejected)\")\n projects: list[str] = Field(default=None, description=\"List of project names to assign to the user\")\n options: Union[UserOptions, None] = Field(default=None, description=\"User configuration options to update\")\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"password\": \"newsecurepassword\",\n \"is_admin\": False,\n \"projects\": [\"my-rag-project\", \"my-agent\"]\n }\n })\n\n\nclass ProjectModelUpdate(BaseModel):\n \"\"\"Update project properties.\"\"\"\n name: Union[str, None] = Field(default=None, description=\"New project name\")\n embeddings: Union[str, None] = Field(default=None, description=\"Name of the embedding model\")\n llm: Union[str, None] = Field(default=None, description=\"Name of the LLM to use\")\n system: Union[str, None] = Field(default=None, max_length=50000, description=\"System prompt for the LLM\")\n censorship: Union[str, None] = Field(default=None, max_length=5000, description=\"Censorship message returned when the guard rejects a query\")\n score: Union[float, None] = Field(default=None, description=\"Minimum similarity score threshold\")\n k: Union[int, None] = Field(default=None, description=\"Number of documents to retrieve\")\n connection: Union[str, None] = Field(default=None, max_length=2000, description=\"Database connection string for natural language to SQL queries\")\n tables: Union[str, None] = Field(default=None, description=\"Comma-separated list of allowed database tables\")\n llm_rerank: Union[bool, None] = Field(default=None, description=\"Enable LLM-based reranking\")\n guard: Union[str, None] = Field(default=None, description=\"Id of the guard project used for input checking (stored as the project id, not the name)\")\n human_name: Union[str, None] = Field(default=None, max_length=200, description=\"Human-readable display name\")\n human_description: Union[str, None] = Field(default=None, max_length=2000, description=\"Human-readable project description\")\n tools: Union[str, None] = Field(default=None, description=\"Comma-separated list of enabled tool names\")\n users: list[str] = Field(default=None, description=\"List of usernames to assign to this project\")\n public: Union[bool, None] = Field(default=None, description=\"Whether the project is publicly accessible\")\n default_prompt: Union[str, None] = Field(default=None, max_length=50000, description=\"Default prompt template\")\n\n @field_validator('name')\n @classmethod\n def name_must_be_safe(cls, v):\n if v is not None:\n return validate_safe_name(v, \"Project name\")\n return v\n options: Union[ProjectOptions, None] = Field(default=None, description=\"Project configuration options to update\")\n team_id: Union[int, None] = Field(default=None, description=\"ID of the team that owns this project\")\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"llm\": \"gpt-4o\",\n \"system\": \"You are a helpful assistant.\",\n \"human_name\": \"Updated Project Name\",\n \"k\": 5,\n \"score\": 0.3,\n \"public\": False\n }\n })\n\n\nclass SourceModel(BaseModel):\n \"\"\"Retrieved source document with relevance score.\"\"\"\n source: str = Field(description=\"Source identifier of the retrieved document\")\n keywords: str = Field(description=\"Keywords associated with the source document\")\n text: str = Field(description=\"Text content of the retrieved chunk\")\n score: float = Field(description=\"Relevance score (0.0 to 1.0)\")\n id: str = Field(description=\"Unique identifier of the document chunk\")\n\n\nclass InferenceResponse(BaseModel):\n \"\"\"Response from an inference project.\"\"\"\n question: str = Field(description=\"The original question that was asked\")\n answer: str = Field(description=\"The LLM-generated answer\")\n type: str = Field(description=\"Project type that generated this response\")\n\n\nclass QuestionResponse(InferenceResponse):\n \"\"\"Response from a RAG question with sources.\"\"\"\n sources: Union[list[SourceModel], Union[list[str], None]] = Field(default=None, description=\"Source documents used to generate the answer\")\n image: Union[str, None] = Field(default=None, description=\"Base64-encoded image in the response (vision projects)\")\n\n\nclass ChatResponse(QuestionResponse):\n \"\"\"Response from a chat interaction including conversation ID.\"\"\"\n id: str = Field(description=\"Conversation ID for continuing the chat session\")\n\n\nclass IngestResponse(BaseModel):\n \"\"\"Result of a knowledge base ingestion operation.\"\"\"\n source: str = Field(description=\"Source identifier of the ingested content\")\n documents: int = Field(description=\"Number of documents processed\")\n chunks: int = Field(description=\"Number of chunks created from the documents\")\n\n\nclass ClassifierModel(BaseModel):\n \"\"\"Text classification request.\"\"\"\n sequence: str = Field(description=\"Text to classify\")\n labels: list[str] = Field(description=\"Candidate labels for classification\")\n model: Optional[str] = Field(default=None, description=\"Classifier model to use (defaults to facebook/bart-large-mnli)\")\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"sequence\": \"I need help with my billing account\",\n \"labels\": [\"billing\", \"technical\", \"general\"],\n \"model\": \"facebook/bart-large-mnli\"\n }\n })\n\n\nclass ClassifierResponse(BaseModel):\n \"\"\"Text classification result with scores.\"\"\"\n sequence: str = Field(description=\"The classified text\")\n labels: list[str] = Field(description=\"Labels ordered by confidence (highest first)\")\n scores: list[float] = Field(description=\"Confidence scores corresponding to each label\")\n model: str = Field(description=\"Classifier model used\")\n\n\nclass TeamUser(BaseModel):\n \"\"\"User reference within a team.\"\"\"\n id: int = Field(description=\"Unique user identifier\")\n username: str = Field(description=\"Username of the team member\")\n model_config = ConfigDict(from_attributes=True)\n\n\nclass TeamLLM(BaseModel):\n \"\"\"LLM reference within a team.\"\"\"\n id: int = Field(description=\"Unique LLM identifier\")\n name: str = Field(description=\"Name of the LLM\")\n model_config = ConfigDict(from_attributes=True)\n\n\nclass TeamEmbedding(BaseModel):\n \"\"\"Embedding model reference within a team.\"\"\"\n id: int = Field(description=\"Unique embedding model identifier\")\n name: str = Field(description=\"Name of the embedding model\")\n model_config = ConfigDict(from_attributes=True)\n\n\nclass TeamProject(BaseModel):\n \"\"\"Project reference within a team.\"\"\"\n id: int = Field(description=\"Unique project identifier\")\n name: str = Field(description=\"Name of the project\")\n model_config = ConfigDict(from_attributes=True)\n\n\nclass TeamBranding(BaseModel):\n \"\"\"Team branding configuration for white-labeling.\"\"\"\n primary_color: Union[str, None] = Field(default=None, max_length=20, description=\"Primary color hex code (e.g. '#1976d2')\")\n secondary_color: Union[str, None] = Field(default=None, max_length=20, description=\"Secondary color hex code (e.g. '#ff9800')\")\n logo_url: Union[str, None] = Field(default=None, max_length=300000, description=\"Logo URL or data:image/... URI (the upload widget downscales to keep this well under the cap)\")\n welcome_message: Union[str, None] = Field(default=None, max_length=1000, description=\"Welcome message shown on landing\")\n app_name: Union[str, None] = Field(default=None, max_length=100, description=\"Custom app name (overrides platform default)\")\n model_config = ConfigDict(from_attributes=True)\n\n\nclass TeamOptions(BaseModel):\n \"\"\"Per-team option blob. Empty fields fall back to platform settings.\"\"\"\n smtp_host: Union[str, None] = Field(default=None, max_length=255, description=\"SMTP server hostname (empty = use platform default)\")\n smtp_port: Union[int, None] = Field(default=None, ge=1, le=65535, description=\"SMTP port (587 STARTTLS, 465 implicit TLS)\")\n smtp_user: Union[str, None] = Field(default=None, max_length=255, description=\"SMTP user\")\n smtp_password: Union[str, None] = Field(default=None, max_length=4096, description=\"SMTP password (encrypted at rest, masked on response)\")\n smtp_from: Union[str, None] = Field(default=None, max_length=320, description=\"Default `From` address used by this team\")\n email_default_to: Union[str, None] = Field(default=None, max_length=320, description=\"Default recipient for notifications when caller didn't specify one\")\n model_config = ConfigDict(from_attributes=True)\n\n # The form always submits `smtp_port` even when the user hasn't\n # touched the SMTP section, so the wire value on a fresh team is\n # the empty string. Pydantic's int parser then 422s — fix it by\n # coercing \"\" / whitespace-only to None at the boundary.\n @field_validator(\"smtp_port\", mode=\"before\")\n @classmethod\n def _empty_port_to_none(cls, v):\n if v is None:\n return None\n if isinstance(v, str) and v.strip() == \"\":\n return None\n return v\n\n\nclass TeamMemberBudget(BaseModel):\n \"\"\"A team member's monthly cost cap + their month-to-date spend in the team.\"\"\"\n user_id: int\n username: str\n budget: Union[float, None] = Field(default=None, description=\"Monthly cost cap (null = uncapped)\")\n spending: float = Field(default=0.0, description=\"Month-to-date cost in this team\")\n remaining: Union[float, None] = Field(default=None, description=\"budget - spending (null when uncapped)\")\n\n\nclass TeamMemberBudgetUpdate(BaseModel):\n \"\"\"Set or clear a member's monthly cost cap. null / -1 clears it.\"\"\"\n budget: Union[float, None] = Field(default=None, ge=-1.0, description=\"Monthly cost cap; null or -1 = uncapped\")\n\n\nclass UserTeamBudget(BaseModel):\n \"\"\"A user's own monthly budget view for one team (self-service).\"\"\"\n team_id: int\n team_name: str\n is_admin: bool = Field(default=False, description=\"Whether the user is an admin of this team\")\n budget: Union[float, None] = Field(default=None, description=\"The user's monthly cost cap in this team (null = uncapped)\")\n spending: float = Field(default=0.0, description=\"The user's month-to-date cost in this team\")\n remaining: Union[float, None] = Field(default=None, description=\"budget - spending (null when uncapped)\")\n team_balance: Union[float, None] = Field(default=None, description=\"The team's prepaid wallet balance (null = no wallet; <= 0 = depleted)\")\n\n\nclass UserTeamBudgetsResponse(BaseModel):\n teams: list[UserTeamBudget] = Field(default=[], description=\"The user's per-team budgets\")\n\n\nclass TeamBalanceUpdate(BaseModel):\n \"\"\"Set a team's prepaid wallet balance to an absolute value (platform admins only).\"\"\"\n balance: float = Field(ge=0, description=\"New wallet balance (>= 0). 0 = depleted (all usage stops).\")\n\n\nclass TeamBalanceTopUp(BaseModel):\n \"\"\"Add funds to a team's prepaid wallet (platform admins only). Additive credit.\"\"\"\n amount: float = Field(gt=0, description=\"Amount to add to the wallet (> 0).\")\n\n\nclass TeamBalanceTransaction(BaseModel):\n \"\"\"One prepaid-wallet movement. `amount` is signed: negative = OUT (usage\n debit), positive = IN (top-up / upward adjustment).\"\"\"\n id: int\n team_id: int\n amount: float = Field(description=\"Signed movement amount (negative = out, positive = in)\")\n balance_after: float = Field(description=\"Wallet balance immediately after this movement\")\n kind: str = Field(description=\"usage | topup | adjustment\")\n description: Union[str, None] = Field(default=None)\n actor_username: Union[str, None] = Field(default=None, description=\"Who triggered the movement (null = unknown/deleted)\")\n created_at: Union[datetime, None] = Field(default=None)\n\n\nclass TeamBalanceTransactionsResponse(BaseModel):\n transactions: list[TeamBalanceTransaction] = Field(default=[])\n total: int = Field(default=0)\n\n\nclass PaymentCheckoutRequest(BaseModel):\n \"\"\"Team admin starts a hosted checkout to top up the wallet.\"\"\"\n amount: float = Field(gt=0, le=1_000_000, description=\"Amount to top up (> 0).\")\n provider: str = Field(description=\"Payment provider: stripe | paypal\")\n save_method: bool = Field(default=False, description=\"Save the card for auto-recharge (Stripe only).\")\n\n\nclass PaymentRedirectResponse(BaseModel):\n redirect_url: str = Field(description=\"Provider hosted checkout URL to redirect the user to.\")\n provider_ref: Union[str, None] = Field(default=None)\n\n\nclass AutoRechargeUpdate(BaseModel):\n \"\"\"Set the team's auto-recharge rule. Requires a saved payment method.\"\"\"\n enabled: bool = Field(default=False)\n threshold: Union[float, None] = Field(default=None, ge=0, description=\"Auto-charge when balance falls below this.\")\n amount: Union[float, None] = Field(default=None, gt=0, le=1_000_000, description=\"Amount to charge each time.\")\n\n\nclass SavedMethod(BaseModel):\n provider: Union[str, None] = Field(default=None)\n brand: Union[str, None] = Field(default=None)\n last4: Union[str, None] = Field(default=None)\n\n\nclass TeamPaymentConfigResponse(BaseModel):\n \"\"\"Team-scoped payment state for the wallet page.\"\"\"\n payments_enabled: bool = Field(default=False)\n providers: list[str] = Field(default=[], description=\"Enabled provider names for top-ups\")\n auto_recharge_providers: list[str] = Field(default=[], description=\"Providers that support auto-recharge\")\n currency: str = Field(default=\"EUR\")\n saved_method: Union[SavedMethod, None] = Field(default=None)\n auto_recharge_enabled: bool = Field(default=False)\n auto_recharge_threshold: Union[float, None] = Field(default=None)\n auto_recharge_amount: Union[float, None] = Field(default=None)\n\n\nclass PaymentTransaction(BaseModel):\n id: int\n provider: str\n kind: str = Field(description=\"topup | auto_recharge | setup\")\n amount: float\n currency: str\n status: str = Field(description=\"pending | completed | failed | canceled\")\n created_at: Union[datetime, None] = Field(default=None)\n completed_at: Union[datetime, None] = Field(default=None)\n\n\nclass PaymentTransactionsResponse(BaseModel):\n transactions: list[PaymentTransaction] = Field(default=[])\n total: int = Field(default=0)\n\n\nclass TeamModel(BaseModel):\n \"\"\"Full team details with members, admins, and resources.\"\"\"\n id: int = Field(description=\"Unique team identifier\")\n name: str = Field(description=\"Team name\")\n description: Union[str, None] = Field(default=None, description=\"Human-readable team description\")\n created_at: datetime = Field(default=None, description=\"Timestamp when the team was created\")\n budget: float = Field(default=-1.0, description=\"Team budget cap in configured currency (-1 means unlimited)\")\n spending: Union[float, None] = Field(default=None, description=\"Total amount spent by the team (only present when budget >= 0)\")\n remaining: Union[float, None] = Field(default=None, description=\"Remaining budget (only present when budget >= 0)\")\n balance: Union[float, None] = Field(default=None, description=\"Prepaid wallet balance (null = no wallet; <= 0 = depleted, all usage stops)\")\n users: list[TeamUser] = Field(default=[], description=\"Team members\")\n admins: list[TeamUser] = Field(default=[], description=\"Team administrators\")\n projects: list[TeamProject] = Field(default=[], description=\"Projects owned by this team\")\n llms: list[TeamLLM] = Field(default=[], description=\"LLMs accessible to this team\")\n embeddings: list[TeamEmbedding] = Field(default=[], description=\"Embedding models accessible to this team\")\n image_generators: list[str] = Field(default=[], description=\"Image generator names accessible to this team\")\n audio_generators: list[str] = Field(default=[], description=\"Audio generator names accessible to this team\")\n branding: Union[TeamBranding, None] = Field(default=None, description=\"Team branding configuration for white-labeling\")\n options: Union[TeamOptions, None] = Field(default=None, description=\"Per-team options (e.g. SMTP overrides). Empty fields fall back to platform settings.\")\n model_config = ConfigDict(from_attributes=True)\n\n @field_validator('branding', mode='before')\n @classmethod\n def parse_branding(cls, v):\n if isinstance(v, str):\n try:\n parsed = json.loads(v)\n if not parsed:\n return None\n return TeamBranding(**parsed)\n except (json.JSONDecodeError, Exception):\n return None\n return v\n\n @field_validator('options', mode='before')\n @classmethod\n def parse_options(cls, v):\n # Response-side: decrypt then mask secrets so plaintext never\n # leaves the process. Internal callers read team_db.options\n # directly and decrypt themselves.\n if isinstance(v, str):\n try:\n parsed = json.loads(v)\n except (json.JSONDecodeError, Exception):\n return None\n v = parsed\n if isinstance(v, dict):\n if not v:\n return None\n try:\n from restai.utils.crypto import decrypt_sensitive_options, TEAM_SENSITIVE_KEYS\n from restai.settings import mask_key\n v = decrypt_sensitive_options(dict(v), TEAM_SENSITIVE_KEYS)\n for k in TEAM_SENSITIVE_KEYS:\n if v.get(k):\n v[k] = mask_key(v[k])\n except Exception:\n pass\n try:\n return TeamOptions(**v)\n except Exception:\n return None\n return v\n\n @field_validator('image_generators', mode='before')\n @classmethod\n def parse_image_generators(cls, v):\n if v and len(v) > 0 and hasattr(v[0], 'generator_name'):\n return [item.generator_name for item in v]\n return v\n\n @field_validator('audio_generators', mode='before')\n @classmethod\n def parse_audio_generators(cls, v):\n if v and len(v) > 0 and hasattr(v[0], 'generator_name'):\n return [item.generator_name for item in v]\n return v\n\n\nclass TeamResponse(BaseModel):\n \"\"\"Simplified team reference.\"\"\"\n id: int = Field(description=\"Unique team identifier\")\n name: str = Field(description=\"Team name\")\n\n\nclass TeamModelCreate(BaseModel):\n \"\"\"Create a new team.\"\"\"\n name: str = Field(description=\"Team name (must be unique)\")\n description: Union[str, None] = Field(default=None, max_length=1000, description=\"Human-readable team description\")\n budget: float = Field(default=-1.0, description=\"Team budget cap (-1 means unlimited)\")\n users: list[str] = Field(default=[], description=\"Usernames to add as team members\")\n admins: list[str] = Field(default=[], description=\"Usernames to add as team administrators\")\n projects: list[str] = Field(default=[], description=\"Project names to assign to this team\")\n llms: list[str] = Field(default=[], description=\"LLM names to make accessible to this team\")\n embeddings: list[str] = Field(default=[], description=\"Embedding model names to make accessible to this team\")\n image_generators: list[str] = Field(default=[], description=\"Image generator names to make accessible to this team\")\n audio_generators: list[str] = Field(default=[], description=\"Audio generator names to make accessible to this team\")\n creator_id: Union[int, None] = Field(default=None, description=\"User ID of the team creator (set automatically)\")\n\n @field_validator('name')\n @classmethod\n def name_must_be_safe(cls, v):\n if not v or not v.strip():\n raise ValueError(\"Team name cannot be empty\")\n return v\n\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"name\": \"Engineering Team\",\n \"description\": \"Engineering team\",\n \"users\": [\"alice\", \"bob\"],\n \"admins\": [\"alice\"],\n \"projects\": [],\n \"llms\": [\"gpt-4o\"],\n \"embeddings\": [\"text-embedding-3-small\"]\n }\n })\n\n\nclass TeamModelUpdate(BaseModel):\n \"\"\"Update team properties.\"\"\"\n name: Union[str, None] = Field(default=None, description=\"New team name\")\n description: Union[str, None] = Field(default=None, max_length=1000, description=\"Updated team description\")\n budget: Union[float, None] = Field(default=None, description=\"Team budget cap (-1 means unlimited)\")\n users: list[str] = Field(default=None, description=\"Updated list of member usernames (replaces existing)\")\n admins: list[str] = Field(default=None, description=\"Updated list of admin usernames (replaces existing)\")\n projects: list[str] = Field(default=None, description=\"Updated list of project names (replaces existing)\")\n llms: list[str] = Field(default=None, description=\"Updated list of LLM names (replaces existing)\")\n\n @field_validator('name')\n @classmethod\n def name_must_be_safe(cls, v):\n if v is not None and not v.strip():\n raise ValueError(\"Team name cannot be empty\")\n return v\n embeddings: list[str] = Field(default=None, description=\"Updated list of embedding model names (replaces existing)\")\n branding: Union[TeamBranding, None] = Field(default=None, description=\"Team branding configuration\")\n options: Union[TeamOptions, None] = Field(default=None, description=\"Per-team options (SMTP overrides etc.). Empty fields fall back to platform settings.\")\n image_generators: list[str] = Field(default=None, description=\"Updated list of image generator names (replaces existing)\")\n audio_generators: list[str] = Field(default=None, description=\"Updated list of audio generator names (replaces existing)\")\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"name\": \"engineering-v2\",\n \"description\": \"Updated engineering team\",\n \"users\": [\"alice\", \"bob\", \"charlie\"],\n \"admins\": [\"alice\"]\n }\n })\n\n\nclass TeamsResponse(BaseModel):\n \"\"\"List of teams.\"\"\"\n teams: list[TeamModel] = Field(description=\"List of team objects\")\n\n\nclass OllamaInstanceModel(BaseModel):\n \"\"\"Ollama instance connection details.\"\"\"\n host: str = Field(default=\"localhost\", description=\"Ollama server hostname or IP address\")\n port: int = Field(default=11434, description=\"Ollama server port\")\n\n\nclass OllamaCloudInstanceModel(BaseModel):\n \"\"\"Ollama Cloud connection details — Bearer-authenticated, no local install.\"\"\"\n api_key: str = Field(description=\"Ollama Cloud API key (https://ollama.com/settings/keys)\")\n host: str = Field(default=\"https://ollama.com\", description=\"Override the cloud base URL — usually leave default\")\n\n\nclass OllamaModelInfo(BaseModel):\n \"\"\"Ollama model metadata.\"\"\"\n name: str = Field(description=\"Model name identifier\")\n modified_at: Optional[str] = Field(default=None, description=\"Last modification timestamp\")\n size: Optional[int] = Field(default=None, description=\"Model size in bytes\")\n digest: Optional[str] = Field(default=None, description=\"Model digest hash\")\n details: Optional[Dict[str, Any]] = Field(default=None, description=\"Additional model metadata (family, parameters, quantization, etc.)\")\n capabilities: Optional[List[str]] = Field(default=None, description=\"Model capabilities (e.g. 'completion', 'embedding', 'vision')\")\n embedding_length: Optional[int] = Field(default=None, description=\"Embedding dimension length (for embedding models)\")\n\n\nclass OllamaModelPullRequest(BaseModel):\n \"\"\"Pull a model to an Ollama instance.\"\"\"\n name: str = Field(description=\"Name of the model to pull (e.g. 'llama3', 'nomic-embed-text')\")\n host: str = Field(default=\"localhost\", description=\"Ollama server hostname or IP address\")\n port: int = Field(default=11434, description=\"Ollama server port\")\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"name\": \"llama3\",\n \"host\": \"localhost\",\n \"port\": 11434\n }\n })\n\n\nclass OllamaModelPullResponse(BaseModel):\n \"\"\"Result of an Ollama model pull operation.\"\"\"\n status: str = Field(description=\"Pull operation status (e.g. 'success', 'error')\")\n model: str = Field(description=\"Name of the pulled model\")\n digest: Optional[str] = Field(default=None, description=\"Digest hash of the pulled model\")\n\n\nclass SettingsResponse(BaseModel):\n \"\"\"Current platform settings.\"\"\"\n app_name: str = Field(description=\"Application display name\")\n logo_url: Optional[str] = Field(default=\"\", description=\"Login-page logo URL or data:image/... URI (empty = use default RESTai logo)\")\n hide_branding: bool = Field(description=\"Whether to hide RESTai branding in the UI\")\n max_audio_upload_size: int = Field(description=\"Maximum audio upload file size in bytes\")\n currency: str = Field(default=\"EUR\", description=\"Currency code for cost display (e.g. 'EUR', 'USD')\")\n redis_host: Optional[str] = Field(default=\"\", description=\"Redis host for chat history persistence\")\n redis_port: Optional[str] = Field(default=\"6379\", description=\"Redis port\")\n redis_password: Optional[str] = Field(default=\"\", description=\"Redis password (masked)\")\n redis_database: Optional[str] = Field(default=\"0\", description=\"Redis database number\")\n # Authentication\n auth_disable_local: bool = Field(default=False, description=\"Disable local username/password authentication\")\n sso_auto_create_user: bool = Field(default=False, description=\"Auto-create user on first SSO login\")\n sso_allowed_domains: Optional[str] = Field(default=\"*\", description=\"Comma-separated allowed email domains for SSO\")\n sso_auto_restricted: bool = Field(default=True, description=\"Auto-created SSO users are restricted (read-only)\")\n sso_auto_team_id: Optional[str] = Field(default=\"\", description=\"Team ID to auto-assign SSO users to\")\n # Google OAuth\n sso_google_client_id: Optional[str] = Field(default=\"\", description=\"Google OAuth client ID\")\n sso_google_client_secret: Optional[str] = Field(default=\"\", description=\"Google OAuth client secret (masked)\")\n sso_google_redirect_uri: Optional[str] = Field(default=\"\", description=\"Google OAuth redirect URI\")\n sso_google_scope: Optional[str] = Field(default=\"openid email profile\", description=\"Google OAuth scope\")\n # Microsoft OAuth\n sso_microsoft_client_id: Optional[str] = Field(default=\"\", description=\"Microsoft OAuth client ID\")\n sso_microsoft_client_secret: Optional[str] = Field(default=\"\", description=\"Microsoft OAuth client secret (masked)\")\n sso_microsoft_tenant_id: Optional[str] = Field(default=\"\", description=\"Microsoft OAuth tenant ID\")\n sso_microsoft_redirect_uri: Optional[str] = Field(default=\"\", description=\"Microsoft OAuth redirect URI\")\n sso_microsoft_scope: Optional[str] = Field(default=\"openid email profile\", description=\"Microsoft OAuth scope\")\n # GitHub OAuth\n sso_github_client_id: Optional[str] = Field(default=\"\", description=\"GitHub OAuth client ID\")\n sso_github_client_secret: Optional[str] = Field(default=\"\", description=\"GitHub OAuth client secret (masked)\")\n sso_github_redirect_uri: Optional[str] = Field(default=\"\", description=\"GitHub OAuth redirect URI\")\n sso_github_scope: Optional[str] = Field(default=\"user:email\", description=\"GitHub OAuth scope\")\n # Generic OIDC\n sso_oidc_client_id: Optional[str] = Field(default=\"\", description=\"OIDC client ID\")\n sso_oidc_client_secret: Optional[str] = Field(default=\"\", description=\"OIDC client secret (masked)\")\n sso_oidc_provider_url: Optional[str] = Field(default=\"\", description=\"OIDC provider discovery URL\")\n sso_oidc_redirect_uri: Optional[str] = Field(default=\"\", description=\"OIDC redirect URI\")\n sso_oidc_scopes: Optional[str] = Field(default=\"openid email profile\", description=\"OIDC scopes\")\n sso_oidc_provider_name: Optional[str] = Field(default=\"SSO\", description=\"OIDC provider display name\")\n sso_oidc_email_claim: Optional[str] = Field(default=\"email\", description=\"OIDC email claim field\")\n # GPU\n gpu_enabled: bool = Field(default=False, description=\"Whether GPU features are enabled\")\n gpu_worker_devices: Optional[str] = Field(default=\"\", description=\"Comma-separated GPU indices for worker processes (e.g. '0,1'). Workers that need a single device target use the first index here as `cuda:N` (`config.RESTAI_DEFAULT_DEVICE`).\")\n # MCP\n mcp_enabled: bool = Field(default=False, description=\"Whether the internal MCP server is enabled\")\n # System LLM\n system_llm: Optional[str] = Field(default=\"\", description=\"Internal LLM used for housekeeping tasks (prompt helpers, summarization, etc)\")\n # Docker\n docker_enabled: bool = Field(default=False, description=\"Whether Docker sandboxed terminal is enabled\")\n docker_url: Optional[str] = Field(default=\"\", description=\"Docker socket or TCP URL for sandboxed terminal\")\n docker_image: Optional[str] = Field(default=\"python:3.12-slim\", description=\"Base image for sandbox containers\")\n docker_timeout: int = Field(default=900, description=\"Container idle timeout in seconds\")\n docker_network: Optional[str] = Field(default=\"none\", description=\"Docker network mode for containers\")\n docker_read_only: bool = Field(default=True, description=\"Mount the sandbox container's rootfs read-only (safer). When false the LLM can pip install.\")\n # Agentic Browser\n browser_enabled: bool = Field(default=False, description=\"Whether the per-chat agentic browser (Playwright/Chromium) is enabled\")\n browser_image: Optional[str] = Field(default=\"mcr.microsoft.com/playwright/python:v1.48.0-jammy\", description=\"Docker image used for browser containers (pre-baked Playwright + Chromium)\")\n browser_network: Optional[str] = Field(default=\"bridge\", description=\"Docker network mode for browser containers (needs outbound — 'bridge' by default)\")\n browser_timeout: int = Field(default=900, description=\"Idle timeout in seconds before a browser container is auto-removed\")\n # Retention\n data_retention_days: int = Field(default=0, description=\"Auto-delete data older than this many days (0 = keep forever)\")\n # 2FA\n enforce_2fa: bool = Field(default=False, description=\"Whether TOTP 2FA is enforced for all local users\")\n # Password rotation\n password_max_age_days: int = Field(default=0, description=\"Soft warning shown on login when a user's password is older than this many days. 0 = disabled.\")\n # Vector DB backends — gating the project-create vectorstore dropdown.\n # ChromaDB defaults on (local PersistentClient when host is empty).\n vectordb_chromadb_enabled: bool = Field(default=True, description=\"Whether ChromaDB appears as an available vectorstore in project creation\")\n vectordb_chromadb_host: Optional[str] = Field(default=\"\", description=\"Remote ChromaDB host. Empty = use a local PersistentClient under the embeddings path.\")\n vectordb_chromadb_port: Optional[str] = Field(default=\"\", description=\"Remote ChromaDB port (only used when host is set)\")\n vectordb_pgvector_enabled: bool = Field(default=False, description=\"Whether PGVector appears as an available vectorstore\")\n vectordb_pgvector_host: Optional[str] = Field(default=\"\", description=\"PGVector PostgreSQL host\")\n vectordb_pgvector_port: Optional[str] = Field(default=\"5432\", description=\"PGVector PostgreSQL port\")\n vectordb_pgvector_user: Optional[str] = Field(default=\"\", description=\"PGVector PostgreSQL user\")\n vectordb_pgvector_password: Optional[str] = Field(default=\"\", description=\"PGVector PostgreSQL password (masked)\")\n vectordb_pgvector_db: Optional[str] = Field(default=\"restai_vectors\", description=\"PGVector PostgreSQL database name\")\n vectordb_weaviate_enabled: bool = Field(default=False, description=\"Whether Weaviate appears as an available vectorstore\")\n vectordb_weaviate_host: Optional[str] = Field(default=\"\", description=\"Weaviate host\")\n vectordb_weaviate_port: Optional[str] = Field(default=\"8080\", description=\"Weaviate HTTP port\")\n vectordb_weaviate_grpc_port: Optional[str] = Field(default=\"50051\", description=\"Weaviate gRPC port\")\n vectordb_weaviate_api_key: Optional[str] = Field(default=\"\", description=\"Weaviate API key (masked, optional)\")\n vectordb_pinecone_enabled: bool = Field(default=False, description=\"Whether Pinecone appears as an available vectorstore\")\n vectordb_pinecone_api_key: Optional[str] = Field(default=\"\", description=\"Pinecone API key (masked)\")\n vectordb_pinecone_index: Optional[str] = Field(default=\"\", description=\"Pinecone index name\")\n # LDAP\n ldap_enabled: bool = Field(default=False, description=\"Whether the /ldap login endpoint accepts authentication\")\n ldap_server_host: Optional[str] = Field(default=\"\", description=\"LDAP server hostname\")\n ldap_server_port: Optional[str] = Field(default=\"\", description=\"LDAP server port (389 plain, 636 LDAPS)\")\n ldap_attribute_for_mail: Optional[str] = Field(default=\"mail\", description=\"LDAP attribute carrying the user's email\")\n ldap_attribute_for_username: Optional[str] = Field(default=\"uid\", description=\"LDAP attribute carrying the username\")\n ldap_search_base: Optional[str] = Field(default=\"\", description=\"LDAP search base DN, e.g. ou=users,dc=example,dc=com\")\n ldap_search_filters: Optional[str] = Field(default=\"\", description=\"Additional LDAP search filters (objectClass etc.); pre-anded with the username match\")\n ldap_app_dn: Optional[str] = Field(default=\"\", description=\"DN of the application bind account used to search for users\")\n ldap_app_password: Optional[str] = Field(default=\"\", description=\"Password for the application bind account (masked)\")\n ldap_use_tls: bool = Field(default=False, description=\"Wrap the LDAP connection in TLS (LDAPS)\")\n ldap_ca_cert_file: Optional[str] = Field(default=\"\", description=\"Path on disk to a CA bundle for LDAPS validation (server-side path)\")\n ldap_ciphers: Optional[str] = Field(default=\"\", description=\"TLS cipher allowlist passed to ldap3. Empty = library default ('ALL')\")\n # SMTP (platform-level). Teams override via team.options.\n smtp_host: Optional[str] = Field(default=\"\", description=\"SMTP server hostname\")\n smtp_port: Optional[str] = Field(default=\"587\", description=\"SMTP server port (587 STARTTLS, 465 implicit TLS)\")\n smtp_user: Optional[str] = Field(default=\"\", description=\"SMTP user\")\n smtp_password: Optional[str] = Field(default=\"\", description=\"SMTP password (masked)\")\n smtp_from: Optional[str] = Field(default=\"\", description=\"Default From address used when sending email\")\n email_default_to: Optional[str] = Field(default=\"\", description=\"Fallback recipient when no `to=` is given (e.g. ops alerts)\")\n # Payments (platform-level). Team admins top up wallets via the enabled providers.\n payment_enabled: Optional[bool] = Field(default=False, description=\"Master toggle for the payment system\")\n payment_stripe_enabled: Optional[bool] = Field(default=False, description=\"Enable Stripe\")\n payment_stripe_secret_key: Optional[str] = Field(default=\"\", description=\"Stripe secret key (masked)\")\n payment_stripe_publishable_key: Optional[str] = Field(default=\"\", description=\"Stripe publishable key\")\n payment_stripe_webhook_secret: Optional[str] = Field(default=\"\", description=\"Stripe webhook signing secret (masked)\")\n payment_paypal_enabled: Optional[bool] = Field(default=False, description=\"Enable PayPal\")\n payment_paypal_client_id: Optional[str] = Field(default=\"\", description=\"PayPal client ID\")\n payment_paypal_client_secret: Optional[str] = Field(default=\"\", description=\"PayPal client secret (masked)\")\n payment_paypal_webhook_id: Optional[str] = Field(default=\"\", description=\"PayPal webhook ID (for signature verification)\")\n payment_paypal_mode: Optional[str] = Field(default=\"sandbox\", description=\"PayPal environment: sandbox or live\")\n\n\nclass SettingsUpdate(BaseModel):\n \"\"\"Update platform settings.\"\"\"\n app_name: Optional[str] = Field(default=None, description=\"Application display name\")\n logo_url: Optional[str] = Field(default=None, max_length=300000, description=\"Login-page logo URL or data:image/... URI\")\n hide_branding: Optional[bool] = Field(default=None, description=\"Whether to hide RESTai branding in the UI\")\n max_audio_upload_size: Optional[int] = Field(default=None, description=\"Maximum audio upload file size in bytes\")\n currency: Optional[str] = Field(default=None, description=\"Currency code for cost display (e.g. 'EUR', 'USD')\")\n redis_host: Optional[str] = Field(default=None, description=\"Redis host for chat history persistence\")\n redis_port: Optional[str] = Field(default=None, description=\"Redis port\")\n redis_password: Optional[str] = Field(default=None, description=\"Redis password\")\n redis_database: Optional[str] = Field(default=None, description=\"Redis database number\")\n # Authentication\n auth_disable_local: Optional[bool] = Field(default=None, description=\"Disable local username/password authentication\")\n sso_auto_create_user: Optional[bool] = Field(default=None, description=\"Auto-create user on first SSO login\")\n sso_allowed_domains: Optional[str] = Field(default=None, description=\"Comma-separated allowed email domains for SSO\")\n sso_auto_restricted: Optional[bool] = Field(default=None, description=\"Auto-created SSO users are restricted (read-only)\")\n sso_auto_team_id: Optional[str] = Field(default=None, description=\"Team ID to auto-assign SSO users to\")\n # Google OAuth\n sso_google_client_id: Optional[str] = Field(default=None, description=\"Google OAuth client ID\")\n sso_google_client_secret: Optional[str] = Field(default=None, description=\"Google OAuth client secret\")\n sso_google_redirect_uri: Optional[str] = Field(default=None, description=\"Google OAuth redirect URI\")\n sso_google_scope: Optional[str] = Field(default=None, description=\"Google OAuth scope\")\n # Microsoft OAuth\n sso_microsoft_client_id: Optional[str] = Field(default=None, description=\"Microsoft OAuth client ID\")\n sso_microsoft_client_secret: Optional[str] = Field(default=None, description=\"Microsoft OAuth client secret\")\n sso_microsoft_tenant_id: Optional[str] = Field(default=None, description=\"Microsoft OAuth tenant ID\")\n sso_microsoft_redirect_uri: Optional[str] = Field(default=None, description=\"Microsoft OAuth redirect URI\")\n sso_microsoft_scope: Optional[str] = Field(default=None, description=\"Microsoft OAuth scope\")\n # GitHub OAuth\n sso_github_client_id: Optional[str] = Field(default=None, description=\"GitHub OAuth client ID\")\n sso_github_client_secret: Optional[str] = Field(default=None, description=\"GitHub OAuth client secret\")\n sso_github_redirect_uri: Optional[str] = Field(default=None, description=\"GitHub OAuth redirect URI\")\n sso_github_scope: Optional[str] = Field(default=None, description=\"GitHub OAuth scope\")\n # Generic OIDC\n sso_oidc_client_id: Optional[str] = Field(default=None, description=\"OIDC client ID\")\n sso_oidc_client_secret: Optional[str] = Field(default=None, description=\"OIDC client secret\")\n sso_oidc_provider_url: Optional[str] = Field(default=None, description=\"OIDC provider discovery URL\")\n sso_oidc_redirect_uri: Optional[str] = Field(default=None, description=\"OIDC redirect URI\")\n sso_oidc_scopes: Optional[str] = Field(default=None, description=\"OIDC scopes\")\n sso_oidc_provider_name: Optional[str] = Field(default=None, description=\"OIDC provider display name\")\n sso_oidc_email_claim: Optional[str] = Field(default=None, description=\"OIDC email claim field\")\n # GPU\n gpu_enabled: Optional[bool] = Field(default=None, description=\"Whether GPU features are enabled\")\n gpu_worker_devices: Optional[str] = Field(default=None, description=\"Comma-separated GPU indices for worker processes (e.g. '0,1')\")\n # MCP\n mcp_enabled: Optional[bool] = Field(default=None, description=\"Whether the internal MCP server is enabled\")\n # System LLM\n system_llm: Optional[str] = Field(default=None, description=\"Internal LLM used for housekeeping tasks\")\n # Docker\n docker_enabled: Optional[bool] = Field(default=None, description=\"Whether Docker sandboxed terminal is enabled\")\n docker_url: Optional[str] = Field(default=None, description=\"Docker socket or TCP URL for sandboxed terminal\")\n docker_image: Optional[str] = Field(default=None, description=\"Base image for sandbox containers\")\n docker_timeout: Optional[int] = Field(default=None, ge=60, description=\"Container idle timeout in seconds\")\n docker_network: Optional[str] = Field(default=None, description=\"Docker network mode for containers\")\n docker_read_only: Optional[bool] = Field(default=None, description=\"Mount the sandbox rootfs read-only (safer). When false the LLM can pip install.\")\n # Agentic Browser\n browser_enabled: Optional[bool] = Field(default=None, description=\"Whether the per-chat agentic browser is enabled\")\n browser_image: Optional[str] = Field(default=None, description=\"Docker image used for browser containers\")\n browser_network: Optional[str] = Field(default=None, description=\"Docker network mode for browser containers\")\n browser_timeout: Optional[int] = Field(default=None, ge=60, description=\"Idle timeout in seconds before browser container removal\")\n # Retention\n data_retention_days: Optional[int] = Field(default=None, ge=0, description=\"Auto-delete data older than this many days (0 = keep forever)\")\n # 2FA\n enforce_2fa: Optional[bool] = Field(default=None, description=\"Whether TOTP 2FA is enforced for all local users\")\n # Password rotation\n password_max_age_days: Optional[int] = Field(default=None, ge=0, description=\"Soft warning shown on login when a user's password is older than this many days. 0 = disabled.\")\n # Vector DB backends\n vectordb_chromadb_enabled: Optional[bool] = Field(default=None, description=\"Whether ChromaDB is offered as a vectorstore option\")\n vectordb_chromadb_host: Optional[str] = Field(default=None, description=\"Remote ChromaDB host (empty for local PersistentClient)\")\n vectordb_chromadb_port: Optional[str] = Field(default=None, description=\"Remote ChromaDB port\")\n vectordb_pgvector_enabled: Optional[bool] = Field(default=None, description=\"Whether PGVector is offered as a vectorstore option\")\n vectordb_pgvector_host: Optional[str] = Field(default=None, description=\"PGVector PostgreSQL host\")\n vectordb_pgvector_port: Optional[str] = Field(default=None, description=\"PGVector PostgreSQL port\")\n vectordb_pgvector_user: Optional[str] = Field(default=None, description=\"PGVector PostgreSQL user\")\n vectordb_pgvector_password: Optional[str] = Field(default=None, description=\"PGVector PostgreSQL password\")\n vectordb_pgvector_db: Optional[str] = Field(default=None, description=\"PGVector PostgreSQL database name\")\n vectordb_weaviate_enabled: Optional[bool] = Field(default=None, description=\"Whether Weaviate is offered as a vectorstore option\")\n vectordb_weaviate_host: Optional[str] = Field(default=None, description=\"Weaviate host\")\n vectordb_weaviate_port: Optional[str] = Field(default=None, description=\"Weaviate HTTP port\")\n vectordb_weaviate_grpc_port: Optional[str] = Field(default=None, description=\"Weaviate gRPC port\")\n vectordb_weaviate_api_key: Optional[str] = Field(default=None, description=\"Weaviate API key (optional)\")\n vectordb_pinecone_enabled: Optional[bool] = Field(default=None, description=\"Whether Pinecone is offered as a vectorstore option\")\n vectordb_pinecone_api_key: Optional[str] = Field(default=None, description=\"Pinecone API key\")\n vectordb_pinecone_index: Optional[str] = Field(default=None, description=\"Pinecone index name\")\n # LDAP\n ldap_enabled: Optional[bool] = Field(default=None, description=\"Whether the /ldap login endpoint accepts authentication\")\n ldap_server_host: Optional[str] = Field(default=None, description=\"LDAP server hostname\")\n ldap_server_port: Optional[str] = Field(default=None, description=\"LDAP server port\")\n ldap_attribute_for_mail: Optional[str] = Field(default=None, description=\"LDAP attribute carrying the user's email\")\n ldap_attribute_for_username: Optional[str] = Field(default=None, description=\"LDAP attribute carrying the username\")\n ldap_search_base: Optional[str] = Field(default=None, description=\"LDAP search base DN\")\n ldap_search_filters: Optional[str] = Field(default=None, description=\"Additional LDAP search filters\")\n ldap_app_dn: Optional[str] = Field(default=None, description=\"Application bind account DN\")\n ldap_app_password: Optional[str] = Field(default=None, description=\"Application bind account password\")\n ldap_use_tls: Optional[bool] = Field(default=None, description=\"Wrap the LDAP connection in TLS (LDAPS)\")\n ldap_ca_cert_file: Optional[str] = Field(default=None, description=\"Path to a CA bundle for LDAPS validation\")\n ldap_ciphers: Optional[str] = Field(default=None, description=\"TLS cipher allowlist (empty = ldap3 default)\")\n # SMTP (platform-level)\n smtp_host: Optional[str] = Field(default=None, description=\"SMTP server hostname\")\n smtp_port: Optional[str] = Field(default=None, description=\"SMTP server port\")\n smtp_user: Optional[str] = Field(default=None, description=\"SMTP user\")\n smtp_password: Optional[str] = Field(default=None, description=\"SMTP password\")\n smtp_from: Optional[str] = Field(default=None, description=\"Default From address\")\n email_default_to: Optional[str] = Field(default=None, description=\"Fallback recipient when no to= is given\")\n # Payments (platform-level)\n payment_enabled: Optional[bool] = Field(default=None, description=\"Master toggle for the payment system\")\n payment_stripe_enabled: Optional[bool] = Field(default=None, description=\"Enable Stripe\")\n payment_stripe_secret_key: Optional[str] = Field(default=None, description=\"Stripe secret key\")\n payment_stripe_publishable_key: Optional[str] = Field(default=None, description=\"Stripe publishable key\")\n payment_stripe_webhook_secret: Optional[str] = Field(default=None, description=\"Stripe webhook signing secret\")\n payment_paypal_enabled: Optional[bool] = Field(default=None, description=\"Enable PayPal\")\n payment_paypal_client_id: Optional[str] = Field(default=None, description=\"PayPal client ID\")\n payment_paypal_client_secret: Optional[str] = Field(default=None, description=\"PayPal client secret\")\n payment_paypal_webhook_id: Optional[str] = Field(default=None, description=\"PayPal webhook ID\")\n payment_paypal_mode: Optional[str] = Field(default=None, description=\"PayPal environment: sandbox or live\")\n model_config = ConfigDict(json_schema_extra={\n \"example\": {\n \"app_name\": \"My AI Platform\",\n \"hide_branding\": True,\n \"currency\": \"USD\"\n }\n })\n\n\nclass ProjectCommentCreate(BaseModel):\n \"\"\"Create a comment on a project.\"\"\"\n content: str = Field(max_length=5000, description=\"Comment text\")\n\nclass ProjectCommentUpdate(BaseModel):\n \"\"\"Update a comment.\"\"\"\n content: str = Field(max_length=5000, description=\"Updated comment text\")\n\nclass ProjectCommentResponse(BaseModel):\n \"\"\"Project comment with user info.\"\"\"\n id: int = Field(description=\"Comment ID\")\n project_id: int = Field(description=\"Project ID\")\n username: str = Field(description=\"Author username\")\n content: str = Field(description=\"Comment text\")\n created_at: datetime = Field(description=\"When the comment was created\")\n updated_at: datetime = Field(description=\"When the comment was last edited\")\n\n\nclass DeleteResponse(BaseModel):\n \"\"\"Confirmation of resource deletion.\"\"\"\n deleted: str = Field(description=\"Identifier of the deleted resource\")\n\n\nclass MessageResponse(BaseModel):\n \"\"\"Generic operation response.\"\"\"\n message: str = Field(description=\"Status message\")\n\n\nclass OpenAIToolCall(BaseModel):\n \"\"\"A tool call returned by the model.\"\"\"\n id: str = Field(description=\"Unique tool call identifier\")\n type: str = Field(default=\"function\", description=\"Tool type\")\n function: dict = Field(description=\"Function name and arguments\")\n\n\nclass OpenAIChatMessage(BaseModel):\n \"\"\"A single message in an OpenAI chat completion request/response.\"\"\"\n role: str = Field(description=\"Message role: 'system', 'user', 'assistant', or 'tool'\")\n content: Optional[str] = Field(default=None, description=\"Message content\")\n tool_calls: Optional[list[OpenAIToolCall]] = Field(default=None, description=\"Tool calls made by the assistant\")\n tool_call_id: Optional[str] = Field(default=None, description=\"ID of the tool call this message responds to\")\n name: Optional[str] = Field(default=None, description=\"Name of the tool or function\")\n refusal: Optional[str] = Field(default=None, description=\"Refusal message, if the model declined\")\n\n\nclass OpenAIFunctionDef(BaseModel):\n \"\"\"Function definition for tool calling.\"\"\"\n name: str = Field(description=\"Function name\")\n description: Optional[str] = Field(default=None, description=\"What the function does\")\n parameters: Optional[dict] = Field(default=None, description=\"JSON Schema for function parameters\")\n\n\nclass OpenAIToolDef(BaseModel):\n \"\"\"Tool definition in an OpenAI chat completion request.\"\"\"\n type: str = Field(default=\"function\", description=\"Tool type\")\n function: OpenAIFunctionDef = Field(description=\"Function definition\")\n\n\nclass OpenAIChatCompletionRequest(BaseModel):\n \"\"\"OpenAI-compatible chat completion request.\n\n `extra=\"allow\"` so unknown OpenAI params (e.g. `metadata`, `store`,\n `service_tier`, `reasoning_effort`) survive parsing and can be forwarded\n verbatim on the native-passthrough path.\"\"\"\n model_config = ConfigDict(extra=\"allow\")\n\n model: str = Field(description=\"Model name to use\")\n messages: list[OpenAIChatMessage] = Field(description=\"List of messages in the conversation\")\n temperature: Optional[float] = Field(default=None, description=\"Sampling temperature (0-2)\")\n max_tokens: Optional[int] = Field(default=None, description=\"Maximum tokens to generate (deprecated alias of max_completion_tokens)\")\n max_completion_tokens: Optional[int] = Field(default=None, description=\"Maximum tokens to generate\")\n stream: bool = Field(default=False, description=\"Enable streaming response via SSE\")\n top_p: Optional[float] = Field(default=None, description=\"Nucleus sampling threshold (0-1)\")\n frequency_penalty: Optional[float] = Field(default=None, description=\"Frequency penalty (-2.0 to 2.0)\")\n presence_penalty: Optional[float] = Field(default=None, description=\"Presence penalty (-2.0 to 2.0)\")\n stop: Optional[Union[str, list[str]]] = Field(default=None, description=\"Stop sequence(s)\")\n seed: Optional[int] = Field(default=None, description=\"Seed for deterministic generation\")\n response_format: Optional[dict] = Field(default=None, description='Response format, e.g. {\"type\": \"json_object\"} or {\"type\":\"json_schema\", ...}')\n n: Optional[int] = Field(default=None, ge=1, le=128, description=\"Number of completions to generate\")\n logprobs: Optional[bool] = Field(default=None, description=\"Return log probabilities\")\n top_logprobs: Optional[int] = Field(default=None, ge=0, le=20, description=\"Number of top logprobs per token\")\n logit_bias: Optional[dict] = Field(default=None, description=\"Token bias map\")\n tools: Optional[list[OpenAIToolDef]] = Field(default=None, description=\"Tool definitions for function calling\")\n tool_choice: Optional[Union[str, dict]] = Field(default=None, description=\"Tool choice: 'auto', 'none', 'required', or specific tool\")\n parallel_tool_calls: Optional[bool] = Field(default=None, description=\"Allow the model to call multiple tools in parallel\")\n functions: Optional[list[dict]] = Field(default=None, description=\"Legacy function definitions (mapped to tools)\")\n function_call: Optional[Union[str, dict]] = Field(default=None, description=\"Legacy function_call (mapped to tool_choice)\")\n stream_options: Optional[dict] = Field(default=None, description='Streaming options, e.g. {\"include_usage\": true}')\n user: Optional[str] = Field(default=None, description=\"End-user identifier\")\n\n\nclass OpenAIChatCompletionChoice(BaseModel):\n \"\"\"A single choice in a chat completion response.\"\"\"\n index: int = Field(description=\"Choice index\")\n message: OpenAIChatMessage = Field(description=\"The generated message\")\n finish_reason: Optional[str] = Field(default=\"stop\", description=\"Reason the generation stopped\")\n logprobs: Optional[dict] = Field(default=None, description=\"Log-probability info, when requested\")\n\n\nclass OpenAIChatCompletionUsage(BaseModel):\n \"\"\"Token usage info for a chat completion.\"\"\"\n prompt_tokens: int = Field(description=\"Number of input tokens\")\n completion_tokens: int = Field(description=\"Number of output tokens\")\n total_tokens: int = Field(description=\"Total tokens used\")\n prompt_tokens_details: Optional[dict] = Field(default=None, description=\"Breakdown of prompt tokens\")\n completion_tokens_details: Optional[dict] = Field(default=None, description=\"Breakdown of completion tokens\")\n\n\nclass OpenAIChatCompletionResponse(BaseModel):\n \"\"\"OpenAI-compatible chat completion response.\"\"\"\n id: str = Field(description=\"Unique completion identifier\")\n object: str = Field(default=\"chat.completion\", description=\"Object type\")\n created: int = Field(description=\"Unix timestamp of creation\")\n model: str = Field(description=\"Model used\")\n choices: list[OpenAIChatCompletionChoice] = Field(description=\"Generated completions\")\n usage: OpenAIChatCompletionUsage = Field(description=\"Token usage statistics\")\n system_fingerprint: Optional[str] = Field(default=None, description=\"System fingerprint\")\n\n\nclass OpenAIAudioTranscriptionResponse(BaseModel):\n \"\"\"OpenAI-compatible audio transcription response.\"\"\"\n text: str = Field(description=\"Transcribed text\")\n\n\nclass OpenAIEmbeddingRequest(BaseModel):\n \"\"\"OpenAI-compatible embedding request.\"\"\"\n model: str = Field(description=\"Embedding model name to use\")\n input: Union[str, list[str]] = Field(description=\"Text(s) to embed\")\n dimensions: Optional[int] = Field(default=None, description=\"Truncate output vectors to this many dimensions\")\n encoding_format: Optional[str] = Field(default=\"float\", description=\"'float' (default) or 'base64'\")\n user: Optional[str] = Field(default=None, description=\"End-user identifier\")\n\n\nclass OpenAIEmbeddingData(BaseModel):\n \"\"\"A single embedding result. `embedding` is a float list, or a base64\n string when `encoding_format='base64'` was requested.\"\"\"\n object: str = Field(default=\"embedding\")\n embedding: Union[list[float], str] = Field(description=\"The embedding vector (or base64 string)\")\n index: int = Field(description=\"Index of the input text\")\n\n\nclass OpenAIEmbeddingUsage(BaseModel):\n \"\"\"Token usage for an embedding request.\"\"\"\n prompt_tokens: int = Field(description=\"Number of input tokens\")\n total_tokens: int = Field(description=\"Total tokens used\")\n\n\nclass OpenAIEmbeddingResponse(BaseModel):\n \"\"\"OpenAI-compatible embedding response.\"\"\"\n object: str = Field(default=\"list\")\n data: list[OpenAIEmbeddingData] = Field(description=\"Embedding results\")\n model: str = Field(description=\"Model used\")\n usage: OpenAIEmbeddingUsage = Field(description=\"Token usage statistics\")\n\n\nclass OpenAIModelObject(BaseModel):\n \"\"\"OpenAI-compatible model object (GET /v1/models/{id}).\"\"\"\n id: str = Field(description=\"Model id\")\n object: str = Field(default=\"model\")\n created: int = Field(default=0, description=\"Unix timestamp\")\n owned_by: str = Field(default=\"restai\", description=\"Owner / provider class\")\n\n\nclass OpenAICompletionRequest(BaseModel):\n \"\"\"OpenAI-compatible legacy text-completion request. `extra='allow'` keeps\n unknown params for native passthrough.\"\"\"\n model_config = ConfigDict(extra=\"allow\")\n\n model: str = Field(description=\"Model name to use\")\n prompt: Union[str, list[str]] = Field(description=\"Prompt text(s)\")\n max_tokens: Optional[int] = Field(default=None, description=\"Maximum tokens to generate\")\n temperature: Optional[float] = Field(default=None, description=\"Sampling temperature\")\n top_p: Optional[float] = Field(default=None, description=\"Nucleus sampling threshold\")\n n: Optional[int] = Field(default=None, ge=1, le=128, description=\"Number of completions\")\n stream: bool = Field(default=False, description=\"Enable streaming via SSE\")\n stop: Optional[Union[str, list[str]]] = Field(default=None, description=\"Stop sequence(s)\")\n seed: Optional[int] = Field(default=None, description=\"Seed for deterministic generation\")\n frequency_penalty: Optional[float] = Field(default=None, description=\"Frequency penalty\")\n presence_penalty: Optional[float] = Field(default=None, description=\"Presence penalty\")\n logit_bias: Optional[dict] = Field(default=None, description=\"Token bias map\")\n stream_options: Optional[dict] = Field(default=None, description=\"Streaming options\")\n user: Optional[str] = Field(default=None, description=\"End-user identifier\")\n\n\nclass OpenAICompletionChoice(BaseModel):\n \"\"\"A single choice in a legacy text-completion response.\"\"\"\n text: str = Field(description=\"Generated text\")\n index: int = Field(description=\"Choice index\")\n logprobs: Optional[dict] = Field(default=None, description=\"Log-probability info\")\n finish_reason: Optional[str] = Field(default=\"stop\", description=\"Reason the generation stopped\")\n\n\nclass OpenAICompletionResponse(BaseModel):\n \"\"\"OpenAI-compatible legacy text-completion response.\"\"\"\n id: str = Field(description=\"Unique completion identifier\")\n object: str = Field(default=\"text_completion\")\n created: int = Field(description=\"Unix timestamp of creation\")\n model: str = Field(description=\"Model used\")\n choices: list[OpenAICompletionChoice] = Field(description=\"Generated completions\")\n usage: OpenAIChatCompletionUsage = Field(description=\"Token usage statistics\")\n system_fingerprint: Optional[str] = Field(default=None, description=\"System fingerprint\")\n\n\nclass OpenAIModerationRequest(BaseModel):\n \"\"\"OpenAI-compatible moderation request.\"\"\"\n input: Union[str, list[str]] = Field(description=\"Text(s) to classify\")\n model: Optional[str] = Field(default=\"text-moderation-latest\", description=\"Moderation model name\")\n\n\nclass OpenAIModerationResult(BaseModel):\n \"\"\"A single moderation result.\"\"\"\n flagged: bool = Field(description=\"Whether the content was flagged\")\n categories: dict = Field(description=\"Per-category boolean flags\")\n category_scores: dict = Field(description=\"Per-category scores\")\n\n\nclass OpenAIModerationResponse(BaseModel):\n \"\"\"OpenAI-compatible moderation response.\"\"\"\n id: str = Field(description=\"Unique moderation identifier\")\n model: str = Field(description=\"Model used\")\n results: list[OpenAIModerationResult] = Field(description=\"Moderation results, one per input\")\n\n\n# ── Evaluation Framework ─────────────────────────────────────────────────\n\n\nclass EvalTestCaseCreate(BaseModel):\n \"\"\"Create a test case for an evaluation dataset.\"\"\"\n question: str = Field(max_length=100000, description=\"Test question to ask the project\")\n expected_answer: Optional[str] = Field(default=None, max_length=100000, description=\"Expected ground truth answer\")\n context: Optional[list[str]] = Field(default=None, description=\"Reference context passages for faithfulness evaluation\")\n\n\nclass EvalTestCaseResponse(BaseModel):\n \"\"\"A test case in an evaluation dataset.\"\"\"\n id: int\n question: str\n expected_answer: Optional[str] = None\n context: Optional[list[str]] = None\n created_at: Optional[datetime] = None\n model_config = ConfigDict(from_attributes=True)\n\n @field_validator('context', mode='before')\n @classmethod\n def parse_context(cls, v):\n if isinstance(v, str):\n try:\n return json.loads(v)\n except (json.JSONDecodeError, TypeError):\n return None\n return v\n\n\nclass EvalTestCaseUpdate(BaseModel):\n \"\"\"Update a test case in an evaluation dataset. Only sent fields change;\n an empty-string expected_answer clears the ground truth.\"\"\"\n question: Optional[str] = Field(default=None, max_length=100000, description=\"Test question to ask the project\")\n expected_answer: Optional[str] = Field(default=None, max_length=100000, description=\"Expected ground truth answer ('' clears it)\")\n context: Optional[list[str]] = Field(default=None, description=\"Reference context passages for faithfulness evaluation\")\n\n\nclass EvalDatasetCreate(BaseModel):\n \"\"\"Create an evaluation dataset for a project.\"\"\"\n name: str = Field(max_length=255, description=\"Dataset name\")\n description: Optional[str] = Field(default=None, max_length=2000, description=\"Dataset description\")\n test_cases: Optional[list[EvalTestCaseCreate]] = Field(default=None, description=\"Initial test cases to add\")\n\n\nclass EvalDatasetUpdate(BaseModel):\n \"\"\"Update an evaluation dataset.\"\"\"\n name: Optional[str] = Field(default=None, max_length=255)\n description: Optional[str] = Field(default=None, max_length=2000)\n\n\nclass EvalDatasetResponse(BaseModel):\n \"\"\"An evaluation dataset summary.\"\"\"\n id: int\n name: str\n description: Optional[str] = None\n project_id: int\n test_case_count: int = 0\n created_at: Optional[datetime] = None\n updated_at: Optional[datetime] = None\n model_config = ConfigDict(from_attributes=True)\n\n\nclass EvalDatasetDetailResponse(EvalDatasetResponse):\n \"\"\"An evaluation dataset with all test cases.\"\"\"\n test_cases: list[EvalTestCaseResponse] = []\n\n\nclass PromptVersionResponse(BaseModel):\n \"\"\"A saved version of a project's system prompt.\"\"\"\n id: int\n project_id: int\n version: int\n system_prompt: str\n description: Optional[str] = None\n created_by: Optional[int] = None\n created_at: Optional[datetime] = None\n is_active: bool = False\n model_config = ConfigDict(from_attributes=True)\n\n\nclass EvalRunCreate(BaseModel):\n \"\"\"Start an evaluation run.\"\"\"\n dataset_id: int = Field(description=\"ID of the dataset to evaluate\")\n metrics: list[str] = Field(\n default=[\"answer_relevancy\"],\n description=\"Metrics to evaluate: answer_relevancy, faithfulness, correctness\"\n )\n prompt_version_id: Optional[int] = Field(default=None, description=\"Prompt version to evaluate (default: current active)\")\n\n\nclass EvalResultResponse(BaseModel):\n \"\"\"A single evaluation result for one test case and one metric.\n `question`/`expected_answer` are joined in from the test case by the\n run-detail endpoint so the results modal can show full context.\"\"\"\n id: int\n test_case_id: int\n question: Optional[str] = None\n expected_answer: Optional[str] = None\n actual_answer: Optional[str] = None\n metric_name: str\n score: Optional[float] = None\n reason: Optional[str] = None\n passed: bool = False\n latency_ms: Optional[int] = None\n model_config = ConfigDict(from_attributes=True)\n\n\nclass EvalRunResponse(BaseModel):\n \"\"\"An evaluation run summary.\"\"\"\n id: int\n dataset_id: int\n project_id: int\n prompt_version_id: Optional[int] = None\n prompt_version: Optional[int] = None # per-project version number (prompt_versions.version), not the FK\n status: str\n metrics: list[str] = []\n summary: Optional[dict] = None\n started_at: Optional[datetime] = None\n completed_at: Optional[datetime] = None\n created_at: Optional[datetime] = None\n error: Optional[str] = None\n model_config = ConfigDict(from_attributes=True)\n\n @field_validator('metrics', mode='before')\n @classmethod\n def parse_metrics(cls, v):\n if isinstance(v, str):\n try:\n return json.loads(v)\n except (json.JSONDecodeError, TypeError):\n return []\n return v or []\n\n @field_validator('summary', mode='before')\n @classmethod\n def parse_summary(cls, v):\n if isinstance(v, str) and v:\n try:\n return json.loads(v)\n except (json.JSONDecodeError, TypeError):\n return None\n return v\n\n\nclass EvalRunDetailResponse(EvalRunResponse):\n \"\"\"An evaluation run with all results.\"\"\"\n results: list[EvalResultResponse] = []\n\n\n# ── Guard Analytics ──────────────────────────────────────────────────────\n\n\nclass GuardEventResponse(BaseModel):\n \"\"\"A single guard event.\"\"\"\n id: int\n phase: str\n action: str\n mode: str\n text_checked: Optional[str] = None\n guard_response: Optional[str] = None\n guard_project: str\n date: Optional[datetime] = None\n user_id: Optional[int] = None\n model_config = ConfigDict(from_attributes=True)\n\n\n# ── Widget Models ────────────────────────────────────────────────────────\n\n\nclass WidgetConfig(BaseModel):\n \"\"\"Visual configuration for a chat widget.\"\"\"\n title: str = Field(default=\"AI Assistant\", max_length=100)\n subtitle: str = Field(default=\"Ask me anything\", max_length=200)\n primaryColor: str = Field(default=\"#6366f1\", max_length=20)\n textColor: str = Field(default=\"#ffffff\", max_length=20)\n position: Literal[\"left\", \"right\"] = Field(default=\"right\")\n welcomeMessage: str = Field(default=\"\", max_length=500)\n avatarUrl: str = Field(default=\"\", max_length=500)\n stream: bool = Field(default=False)\n context_prefix: bool = Field(default=True, description=\"Auto-prepend user context block to system prompt\")\n\n @field_validator(\"avatarUrl\")\n @classmethod\n def _validate_avatar_url(cls, v: str) -> str:\n \"\"\"Avatar URL ends up in the widget's `<img src=...>` rendered\n on every embedding site. The widget runtime additionally\n attribute-escapes the value, but server-side scheme allowlist\n is the durable defense — we don't want to ship a malicious URL\n to clients in the first place. Allowed:\n\n - empty / None (use default bot icon)\n - `http://...` and `https://...`\n - `data:image/...` (inline base64 — widely used for\n self-contained branded widgets)\n\n Anything else (`javascript:`, attribute-breakout payloads,\n unknown schemes) is rejected.\n \"\"\"\n if v is None or v == \"\":\n return \"\"\n s = str(v).strip()\n if not s:\n return \"\"\n # Reject control characters and quotes that could break out of\n # the attribute even with a benign-looking scheme prefix.\n if any(c in s for c in ('\"', \"'\", \"<\", \">\", \"\\r\", \"\\n\", \"\\t\")):\n raise ValueError(\n \"avatarUrl contains disallowed characters (quotes / angle brackets / whitespace control)\"\n )\n lo = s.lower()\n if lo.startswith(\"http://\") or lo.startswith(\"https://\"):\n return s\n if lo.startswith(\"data:image/\"):\n return s\n raise ValueError(\n \"avatarUrl must use http(s):// or data:image/... ; other schemes are not allowed\"\n )\n\n\nVALID_TEMPLATE_VISIBILITIES = (\"private\", \"team\", \"public\")\n\n\nclass ProjectTemplatePublish(BaseModel):\n \"\"\"Snapshot the calling project into a new template.\"\"\"\n name: str = Field(min_length=1, max_length=255, description=\"Template display name\")\n description: Optional[str] = Field(default=None, max_length=2000, description=\"Short description shown in the library list\")\n visibility: Literal[\"private\", \"team\", \"public\"] = Field(default=\"private\", description=\"Who can see/use this template\")\n\n\nclass ProjectTemplateUpdate(BaseModel):\n name: Optional[str] = Field(default=None, min_length=1, max_length=255)\n description: Optional[str] = Field(default=None, max_length=2000)\n visibility: Optional[Literal[\"private\", \"team\", \"public\"]] = Field(default=None)\n\n\nclass ProjectTemplateInstantiate(BaseModel):\n \"\"\"Create a fresh project from a template.\"\"\"\n name: str = Field(min_length=1, max_length=255)\n team_id: int = Field(description=\"Target team for the new project\")\n llm: Optional[str] = Field(default=None, description=\"LLM to use; defaults to the template's suggested_llm if you have access\")\n embeddings: Optional[str] = Field(default=None, description=\"Embeddings model (RAG only)\")\n\n\nclass ProjectTemplateResponse(BaseModel):\n id: int\n name: str\n description: Optional[str] = None\n project_type: str\n suggested_llm: Optional[str] = None\n suggested_embeddings: Optional[str] = None\n visibility: str\n creator_username: Optional[str] = None\n team_id: Optional[int] = None\n team_name: Optional[str] = None\n created_at: datetime\n use_count: int = 0\n model_config = ConfigDict(from_attributes=True)\n\n\nclass WidgetCreate(BaseModel):\n \"\"\"Create a new widget for a project.\"\"\"\n name: str = Field(default=\"Chat Widget\", max_length=255)\n config: WidgetConfig = Field(default_factory=WidgetConfig)\n allowed_domains: list[str] = Field(default=[])\n\n\nclass WidgetUpdate(BaseModel):\n \"\"\"Update widget configuration.\"\"\"\n name: Optional[str] = Field(default=None, max_length=255)\n config: Optional[WidgetConfig] = None\n allowed_domains: Optional[list[str]] = None\n enabled: Optional[bool] = None\n\n\nclass WidgetResponse(BaseModel):\n \"\"\"Widget metadata returned to project owners.\"\"\"\n id: int\n project_id: int\n name: str\n config: WidgetConfig\n allowed_domains: list[str]\n enabled: bool\n key_prefix: str\n widget_key: Optional[str] = None\n has_context_secret: bool = False\n created_at: datetime\n updated_at: datetime\n model_config = ConfigDict(from_attributes=True)\n\n @model_validator(mode='before')\n @classmethod\n def decrypt_widget_key(cls, values):\n if hasattr(values, 'encrypted_key') and values.encrypted_key:\n try:\n from restai.utils.crypto import decrypt_api_key\n if hasattr(values, '__dict__'):\n values.__dict__['widget_key'] = decrypt_api_key(values.encrypted_key)\n values.__dict__['has_context_secret'] = bool(values.context_secret)\n except Exception:\n pass\n elif isinstance(values, dict) and values.get('encrypted_key'):\n try:\n from restai.utils.crypto import decrypt_api_key\n values['widget_key'] = decrypt_api_key(values['encrypted_key'])\n values['has_context_secret'] = bool(values.get('context_secret'))\n except Exception:\n pass\n return values\n\n @field_validator('config', mode='before')\n @classmethod\n def parse_config(cls, v):\n if isinstance(v, str):\n try:\n return WidgetConfig(**json.loads(v))\n except (json.JSONDecodeError, TypeError):\n return WidgetConfig()\n elif isinstance(v, dict):\n return WidgetConfig(**v)\n return v\n\n @field_validator('allowed_domains', mode='before')\n @classmethod\n def parse_domains(cls, v):\n if isinstance(v, str):\n try:\n return json.loads(v)\n except (json.JSONDecodeError, TypeError):\n return []\n return v\n\n\nclass WidgetCreatedResponse(WidgetResponse):\n \"\"\"Returned only at creation time; includes the plaintext widget key.\"\"\"\n widget_key: str\n\n\nclass WidgetChatRequest(BaseModel):\n \"\"\"Minimal chat request from an embedded widget.\"\"\"\n question: str = Field(max_length=10000)\n id: Optional[str] = Field(default=None)\n stream: Optional[bool] = Field(default=None)\n\n\nclass BlockGenerateRequest(BaseModel):\n \"\"\"Generate a Blockly workspace from a natural-language description.\"\"\"\n description: str = Field(max_length=4000, description=\"Plain-English description of what the block project should do\")\n\n\nclass SystemPromptGenerateRequest(BaseModel):\n \"\"\"Generate a project system prompt from a short description.\"\"\"\n description: str = Field(max_length=2000, description=\"Short description of the project's purpose\")\n project_type: Optional[str] = Field(default=None, description=\"Project type: rag, agent, or block (for context)\")\n\n\nclass RoutineCreate(BaseModel):\n \"\"\"Create a scheduled routine for a project.\"\"\"\n name: str = Field(max_length=255, description=\"Routine display name\")\n message: str = Field(max_length=10000, description=\"Message to send to the project when the routine fires\")\n schedule_minutes: int = Field(ge=1, le=10080, description=\"Interval in minutes (1 min to 7 days)\")\n enabled: bool = Field(default=True, description=\"Whether the routine is active\")\n\n\nclass RoutineUpdate(BaseModel):\n \"\"\"Update a routine.\"\"\"\n name: Optional[str] = Field(default=None, max_length=255)\n message: Optional[str] = Field(default=None, max_length=10000)\n schedule_minutes: Optional[int] = Field(default=None, ge=1, le=10080)\n enabled: Optional[bool] = None\n\n\nclass WidgetChatResponse(BaseModel):\n \"\"\"Sanitized response returned to widgets.\"\"\"\n answer: str\n id: Optional[str] = None\n\n\n# ── Knowledge Graph Models ───────────────────────────────────────────────\n\n\nclass KGEntityResponse(BaseModel):\n \"\"\"A single knowledge graph entity.\"\"\"\n id: int\n project_id: int\n name: str\n normalized: str\n entity_type: str\n mention_count: int\n created_at: datetime\n updated_at: datetime\n model_config = ConfigDict(from_attributes=True)\n\n\nclass KGEntityMentionResponse(BaseModel):\n \"\"\"An entity mention within a source.\"\"\"\n source: str\n mention_count: int\n\n\nclass KGEntityDetailResponse(KGEntityResponse):\n \"\"\"Full entity detail including mentions and related entities.\"\"\"\n mentions: list[KGEntityMentionResponse] = []\n related: list[dict] = []\n\n\nclass KGEntityUpdate(BaseModel):\n \"\"\"Update an entity's display name.\"\"\"\n name: str = Field(min_length=1, max_length=255)\n\n\nclass KGEntityMergeRequest(BaseModel):\n \"\"\"Merge one entity into another.\"\"\"\n target_id: int = Field(description=\"ID of the entity to merge INTO\")\n\n\nclass KGGraphNode(BaseModel):\n id: int\n label: str\n type: str\n mention_count: int\n\n\nclass KGGraphEdge(BaseModel):\n from_id: int = Field(alias=\"from\")\n to_id: int = Field(alias=\"to\")\n weight: int\n model_config = ConfigDict(populate_by_name=True)\n\n\nclass KGGraphResponse(BaseModel):\n nodes: list[KGGraphNode]\n edges: list[KGGraphEdge]\n\n\nclass KGDuplicateCandidate(BaseModel):\n entity_a_id: int\n entity_a_name: str\n entity_b_id: int\n entity_b_name: str\n similarity: float\n\n\nclass KGQueryRequest(BaseModel):\n \"\"\"Natural language query against the knowledge graph.\"\"\"\n question: str = Field(min_length=1, max_length=10000)\n\n\nclass KGQueryResponse(BaseModel):\n answer: str\n entities_matched: list[str]\n sources: list[str]\n source_count: int\n\n\n# Rebuild models with forward references to resolve circular dependencies\nTeamModel.model_rebuild()\nUser.model_rebuild()\nProjectModel.model_rebuild()\nProjectResponse.model_rebuild()\nLLMModel.model_rebuild()\nEmbeddingModel.model_rebuild()\n"}}}}