ai_providers
подключения к моделям
ключ at-rest
| id |
BigInteger |
PK |
— |
| name |
Text |
NOT NULL |
отображаемое имя · «OpenAI» · «Ollama (on-prem)» |
| kind |
Text |
NOT NULLDEFAULTCHECK
|
где крутится · cloud · local |
| adapter |
Text |
NOT NULLCHECK
|
диалект API · решает SDK-клиент и discovery · openai ·
anthropic · google · ollama · openai_compatible ·
→ Диалект API
|
| base_url |
Text |
NULLCHECK
|
endpoint · у cloud опционален, у local обязателен (CHECK) |
| api_key_enc |
Text |
NULL |
write-only · AES-256-GCM · NULL у локальных · → ключи |
| is_system |
Boolean |
NOT NULLDEFAULT
|
встроенный рантайм, засеян миграцией · неудаляем, base_url/adapter под управлением · → Встроенный эмбеддер |
| status |
Text |
NOT NULLDEFAULTCHECK
|
итог последней проверки · active · error · unchecked |
| last_check_at |
DateTime(tz) |
NULL |
NULL = не проверяли |
| created_at |
DateTime(tz) |
DEFAULT |
server_default=now() |
| updated_at |
DateTime(tz) |
DEFAULT |
now() + trigger |
ai_models
каталог моделей
FK → ai_providers
UNIQUE(provider_id, model_id)
| id |
BigInteger |
PK |
— |
| provider_id |
BigInteger |
FK→ai_providersIDX
|
CASCADE |
| model_id |
Text |
NOT NULLUNIQUE*
|
идентификатор у провайдера · «gpt-4o» |
| display_name |
Text |
NOT NULL |
отображаемое имя · дефолт = model_id |
| model_type |
Text |
NOT NULLCHECK
|
что выдаёт · chat · embedding · → тип решает назначение |
| origin |
Text |
NOT NULLCHECK
|
как попала в каталог · discovered · manual · builtin (засеяна со встроенным рантаймом) |
| is_enabled |
Boolean |
NOT NULLDEFAULT
|
включена в платформу · DEFAULT false |
| price_input |
Numeric |
NULL |
цена за input-токен · NULL = не задана (локальные) ·
→ Цена вход/выход
|
| price_output |
Numeric |
NULL |
цена за output-токен · NULL у embedding и локальных |
| meta |
JSONB |
NULL |
интринсики модели · у embedding: embedding_dim (задаёт halfvec(N) в KS), max_input_tokens, instruction_prefix · → Интринсики |
| created_at |
DateTime(tz) |
DEFAULT |
now() |
| updated_at |
DateTime(tz) |
DEFAULT |
now() + trigger |
model_assignments
системные функции
FK → ai_models
UNIQUE(function)
| id |
BigInteger |
PK |
— |
| function |
Text |
NOT NULLUNIQUECHECK
|
одна строка на функцию · системные: harvester_embedding ·
query_rag ·
→ Словарь функций
|
| model_id |
BigInteger |
FK→ai_modelsNULL
|
назначенная модель · NULL = ждёт модуль · RESTRICT |
| created_at |
DateTime(tz) |
DEFAULT |
now() |
| updated_at |
DateTime(tz) |
DEFAULT |
now() + trigger |
chat_models
пользовательский чат
FK → ai_models
ровно один default
| id |
BigInteger |
PK |
— |
| model_id |
BigInteger |
FK→ai_modelsUNIQUEIDX
|
разрешённая chat-модель · RESTRICT — в списке = занята |
| is_default |
Boolean |
NOT NULLDEFAULT
|
partial UNIQUE WHERE is_default — ровно один |
| created_at |
DateTime(tz) |
DEFAULT |
now() |
| updated_at |
DateTime(tz) |
DEFAULT |
now() + trigger |
agent_models
модели агентов
FK → ai_models
ровно один default
| id |
BigInteger |
PK |
— · agents.model_id ссылается сюда |
| model_id |
BigInteger |
FK→ai_modelsUNIQUEIDX
|
разрешённая модель агентов · chat-тип, с поддержкой инструментов · RESTRICT — в списке = занята |
| is_default |
Boolean |
NOT NULLDEFAULT
|
partial UNIQUE WHERE is_default — ровно один · преднабор в редакторе агента |
| created_at |
DateTime(tz) |
DEFAULT |
now() |
| updated_at |
DateTime(tz) |
DEFAULT |
now() + trigger |
model_usage
расходы · агрегат по дню
FK → ai_models
UNIQUE(model_id, function, bucket_date)
| id |
BigInteger |
PK |
— |
| model_id |
BigInteger |
FK→ai_modelsIDX
|
SET NULL · расход переживает удаление модели |
| function |
Text |
NOT NULLCHECK
|
какая функция потребила · общий словарь ·
→ Словарь функций
|
| bucket_date |
Date |
NOT NULLUNIQUE*
|
день агрегации |
| request_count |
BigInteger |
NOT NULLDEFAULT
|
DEFAULT 0 |
| input_tokens |
BigInteger |
NOT NULLDEFAULT
|
токены на входе · DEFAULT 0 |
| output_tokens |
BigInteger |
NOT NULLDEFAULT
|
токены на выходе · 0 у embedding |
| cost |
Numeric |
NULL |
input·price_input + output·price_output · NULL если цены не заданы |
| created_at |
DateTime(tz) |
DEFAULT |
now() |
| updated_at |
DateTime(tz) |
DEFAULT |
now() + trigger · последний инкремент бакета |
ORM-модели — в пакете ai_foundation/models.py
(конвенция кода — пакет на модуль), не в admin: назначенные модели
читают
AI
Harvester (embed), Knowledge Store (реэмбеддинг), Query Engine
(RAG) и Agent Engine — четыре потребителя, не только Admin. Admin
предоставляет CRUD и экраны;
зависимости идут к core — без циклов.
api_key_enc — write-only secret, шифротекст
AES-256-GCM крипто-ядром Auth; наружу (API, экспорт) не
возвращается, UI получает лишь маску ••••xxxx. У
локальных провайдеров ключа обычно нет.
Две ортогональные оси. kind (cloud / local) — где
модель крутится: от него зависит, нужен ли ключ и обязателен ли
base_url. adapter — на каком протоколе с
ней говорить: он выбирает SDK-клиент и эндпоинт discovery
(openai → GET /v1/models,
ollama → GET /api/tags).
openai_compatible покрывает vLLM · llama.cpp ·
прокси. Из name (отображаемое имя) протокол не
выводится — потому отдельным полем.
Платформа везёт собственный локальный embeddings-рантайм —
отдельный сервис docker-compose (OpenAI-совместимый,
model-agnostic), поднимается на
make up рядом с
backend; контейнер лёгкий, ML-зависимости в API-образ не тянет.
Веса при установке не качаются: рантайм тянет
их
лениво — по назначению модели (и сразу
прогревает
её, не ожидая первого запроса), в кеш-том. До выбора эмбеддер не занимает ни диска, ни
памяти, и потому не лимитирует деплой. Миграция засевает
is_system-провайдера
Platform
(неудаляем,
base_url на внутренний сервис, без
ключа) и каталог встроенных моделей — выбираемых
(
is_enabled=true), но не загруженных. Набор задаёт
seed, не экраны (первая итерация —
bge-m3 и
Qwen3-Embedding-0.6B, обе 1024-мерные); макеты
ссылаются на них лишь примером. Назначения по умолчанию
нет: строки
model_assignments.harvester_embedding на старте не
существует, и Harvester без назначения приём не стартует —
Admin обязан назначить embedding-модель (встроенную
или свою) до первого приёма. Причина жёсткая: выбор
фиксирует размерность
N колонки
chunks.embedding, а поздняя смена — массовый
переэмбеддинг
(при иной
N — миграция схемы). Дефолт намеренно
пуст: ни модель, ни её вес не навязываются инсталляцией. Как этот
сервис держит потоки нагрузки и масштабируется (v1 → v2) —
embeddings-рантайм.
Параметры, заданные самой моделью — размерность вектора
(
embedding_dim), потолок окна
(
max_input_tokens), обязательный префикс
(
instruction_prefix) — живут в
meta
модели, не в конфиге потребителя (no hardcoding): чанкер
Harvester читает потолок оттуда, KS берёт
embedding_dim назначенной embedding-модели и
провизионит
halfvec(N). Встроенные модели
засеваются со своими интринсиками —
bge-m3:
{ embedding_dim: 1024, max_input_tokens: 8192 },
Qwen3-Embedding-0.6B:
{ 1024, 32768 }. Размерность облачной модели
(
embedding_dim) discovery не возвращает, поэтому админ
задаёт её при добавлении модели; модель без заданной размерности —
или с размерностью, не совпадающей с выделенным
halfvec(N), — отклоняется при
назначении
(409), ещё до старта переэмбеддинга.
Тип решает назначение
model_type (chat / embedding) ограничивает, на какие
функции модель можно назначить: embedding — только в
embedding-функции, chat — в чат, RAG, агентов. Смена типа и
выключение модели заблокированы, пока она назначена
какой-либо функции или состоит в списке чата либо агентов
(FK RESTRICT на всех трёх таблицах): иначе назначение
осталось бы с несовместимым или пустым типом, а список — без модели.
Системная функция — одна строка
model_assignments
(одна модель). Чат и агенты — два списка одной формы:
chat_models (сотрудник выбирает модель диалога) и
agent_models (владелец выбирает модель агента), в
каждом ровно один
is_default и только модели типа
chat. Дефолт защищён симметрично системным функциям: пометку
is_default нельзя снять, не назначив другую, а
модель-дефолт держится в списке через
RESTRICT —
список не остаётся без модели по умолчанию. Связь агента с
выбором —
agents.model_id
→
agent_models (
ON DELETE SET NULL):
сняли модель из списка — ссылка агента обнуляется, и агент встаёт.
Цена и расход — раздельно вход/выход
У cloud-чата output-токен дороже input в разы, поэтому цена —
два поля (price_input · price_output), а
расход копит input_tokens и output_tokens
раздельно. cost = input·price_input +
output·price_output. У embedding выхода нет — price_output
и output_tokens пусты / нули. Валюта пока единая
(доллары), без отдельного поля — мультивалютность отложена.
Единый словарь функций
Набор AI-функций задан один раз (enum в core):
harvester_embedding · query_rag ·
agent_engine · chat.
model_assignments.function CHECK-ается
подмножеством системных двух (harvester_embedding ·
query_rag) — чат и агенты назначаются не здесь, а
списками chat_models / agent_models;
model_usage.function копит расход по полному набору,
включая chat и agent_engine. Один
источник правды — расхождение исключено.
model_usage копит requests / tokens / cost по
(модель · функция · день), не event-log на каждый запрос. Порог
бюджета — настройка инстанса (
ai_monthly_budget +
ai_budget_alert_enabled) и живёт не здесь, а в
platform_settings,
рядом с
maintenance_mode: админ вводит сумму руками и
включает тумблером (CHECK не даёт включить оповещение без суммы) на
экране
Расходы AI,
в общем доме лимитов. При достижении порога поднимается уведомление
типа Budget — канал держит модуль уведомлений. Панорама расхода за
неделю / месяц / год на том же экране — производная
SUM
по периоду, без отдельной таблицы-счётчика. Как устроен учёт целиком —
бизнес-логика расходов.