"""What AI Labs offers, in code. This module is the single source of truth for every model id, every option value and every limit the surface exposes. It is code rather than a table for the same reason the achievement catalogue is: the *meaning* of an entry lives in the executor that reads it, so a DB row could never be the whole definition, and a config table would let production drift from the code that interprets it. **Every value here was measured against the live Singapore endpoint on the date its entry records — nothing is copied from documentation.** That discipline earned its keep twice while this file was being written: * ``wan3.0-video-prime`` **silently ignores the ``size`` parameter.** Three submissions on 2026-09-03 asking for ``1280*720``, ``960*960`` and ``720*1280`` all succeeded and all reported an output ratio unrelated to the request (``1:1``, ``16:9``, ``16:9`` respectively), with ``SR`` pinned at 1080 every time. So this catalogue exposes **no aspect-ratio control** for video: shipping a dropdown that changes nothing would be worse than shipping none. ``resolution`` *is* honoured (``480P`` → SR 480, ``1080P`` → SR 1080), so that is the one quality knob offered. * Half the plausible model ids were returning ``403 AllocationQuota.FreeTierOnly`` because of a per-model console switch, not because they were unavailable. Picking "a stable older model" from the docs would have shipped a dead capability. See :mod:`app.domains.bailian.media_client` for the full story. ``server/scripts/verify_ai_labs_catalog.py`` re-checks this file against the configured key and prints a per-entry verdict. Run it when adding an entry, and after any Bailian console change. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Any from app.domains.bailian.media_client import ( PATH_MULTIMODAL, PATH_VIDEO, ) #: Bump when an entry is added, removed or re-pointed. Surfaced by the admin #: console so an operator can tell which catalogue a deployment is serving. CATALOG_VERSION = "2026-09-03.2" # ── chat models ──────────────────────────────────────────────── #: Tier drives ordering and the badge the UI shows. Not a capability claim. TIER_FLAGSHIP = "flagship" TIER_FAST = "fast" TIER_ALTERNATIVE = "alternative" @dataclass(frozen=True) class ChatModel: """One selectable chat model.""" #: Stable key stored on conversations. Re-pointing ``vendor_model`` later #: keeps existing threads working; renaming a key would orphan them. key: str vendor_model: str tier: str #: Who made it — the UI groups by this so "other vendors' models" is an #: honest label rather than everything looking like Qwen. vendor: str #: Verified by sending a real image part, not assumed from the name. supports_vision: bool #: Whether the vendor accepts ``enable_thinking``. supports_thinking: bool #: Our default for the toggle. False everywhere it is supported: the #: qwen3.7 family defaults to deep thinking and takes 15-18s to emit a #: first token, which reads as a broken page. default_thinking: bool verified_on: str #: Rough first-token latency measured on a one-line prompt, in seconds. #: Shown as a hint so an employee can pick "fast" deliberately. measured_ttfb_seconds: float | None = None CHAT_MODELS: tuple[ChatModel, ...] = ( ChatModel( key="qwen3.8-max", vendor_model="qwen3.8-max", tier=TIER_FLAGSHIP, vendor="qwen", supports_vision=True, supports_thinking=True, default_thinking=False, verified_on="2026-09-03", measured_ttfb_seconds=2.3, ), ChatModel( key="qwen3.8-flash", vendor_model="qwen3.8-flash", tier=TIER_FAST, vendor="qwen", supports_vision=True, supports_thinking=True, default_thinking=False, verified_on="2026-09-03", measured_ttfb_seconds=3.8, ), ChatModel( key="qwen3-max", vendor_model="qwen3-max", tier=TIER_ALTERNATIVE, vendor="qwen", supports_vision=True, supports_thinking=True, default_thinking=False, verified_on="2026-09-03", measured_ttfb_seconds=2.1, ), ChatModel( key="qwen3-vl-plus", vendor_model="qwen3-vl-plus", tier=TIER_ALTERNATIVE, vendor="qwen", supports_vision=True, supports_thinking=True, default_thinking=False, verified_on="2026-09-03", ), ChatModel( key="deepseek-v4-pro", vendor_model="deepseek-v4-pro", tier=TIER_ALTERNATIVE, vendor="deepseek", supports_vision=True, supports_thinking=True, default_thinking=False, verified_on="2026-09-03", measured_ttfb_seconds=11.4, ), ChatModel( key="glm-5.2", vendor_model="glm-5.2", tier=TIER_ALTERNATIVE, vendor="zhipu", supports_vision=True, supports_thinking=True, default_thinking=False, verified_on="2026-09-03", measured_ttfb_seconds=16.5, ), ChatModel( key="kimi-k3", vendor_model="kimi-k3", tier=TIER_ALTERNATIVE, vendor="moonshot", supports_vision=True, supports_thinking=True, default_thinking=False, verified_on="2026-09-03", measured_ttfb_seconds=10.4, ), ) CHAT_MODELS_BY_KEY: dict[str, ChatModel] = {m.key: m for m in CHAT_MODELS} #: What a new conversation gets. Flagship, thinking off — fastest first #: token of the strong models (2.3s measured). DEFAULT_CHAT_MODEL_KEY = "qwen3.8-max" def chat_model(key: str | None) -> ChatModel: """Resolve a stored/requested key, falling back to the default. Never raises: a conversation whose model was removed from the catalogue must keep opening, and an unknown key from a client is a validation concern for the router, not a reason to 500 a history read. """ if key and key in CHAT_MODELS_BY_KEY: return CHAT_MODELS_BY_KEY[key] return CHAT_MODELS_BY_KEY[DEFAULT_CHAT_MODEL_KEY] # ── generation capabilities ──────────────────────────────────── CAP_IMAGE = "image" CAP_IMAGE_EDIT = "image_edit" CAP_VIDEO = "video" CAP_SPEECH = "speech" CAP_TRANSCRIBE = "transcribe" #: How the executor talks to the vendor for a capability. TRANSPORT_SYNC = "sync_multimodal" TRANSPORT_ASYNC = "async_task" #: What lands on the row when the job succeeds. OUTPUT_IMAGE = "image" OUTPUT_VIDEO = "video" OUTPUT_AUDIO = "audio" OUTPUT_TEXT = "text" #: What the employee may attach as input. INPUT_NONE = "none" INPUT_IMAGE = "image" INPUT_AUDIO = "audio" @dataclass(frozen=True) class Option: """One selectable value of a generation parameter.""" value: Any #: Suffix of the i18n key the frontend renders #: (``aiLabs.option..