# Настройка локального LLM-сервера (Qwen3.5, профили compose `llm`/`llm-gpu`)

Плагин суммаризации `qwen_local` (`backend/core/plugins/qwen_local.py`)
обращается к OpenAI-совместимому серверу `llama.cpp`. Модель и параметры —
единый источник истины ADR-004 (`docs/architecture/adr/004-ai-tier-matrix.md`)
и константная матрица `backend/services/ai_tiers.py`. В обычной установке всё
описанное ниже делает `install.sh` (см. `docs/deploy/dev-setup.md`) — этот
раздел актуален для ручного/точечного запуска профиля без инсталлятора.

## 1. Компоненты

| Сервис | Образ | Профиль | Назначение |
|---|---|---|---|
| `llm-models-init` | `busybox` | `llm`/`llm-gpu` | фиксирует владельца тома `llm-models` (обходит дефект прав доступа) |
| `llm-model-init` | `curlimages/curl` | `llm`/`llm-gpu` | однократное скачивание GGUF-модели и tokenizer.json уровня AI |
| `llm` | `ghcr.io/ggml-org/llama.cpp:server-b<build>` | `llm` | CPU-инференс (`/v1/chat/completions`), уровни `min`/`medium` |
| `llm-gpu` | `ghcr.io/ggml-org/llama.cpp:server-cuda-b<build>` | `llm-gpu` | GPU-инференс, уровень `max` (ADR-004: GPU обязателен) |

Файлы:
- `deploy/llm/download-model.sh` — генерализованный скрипт скачивания
  (параметризован `LLM_MODEL_FILE`/`LLM_MODEL_URL`/`LLM_MODEL_MIN_SIZE`/
  `LLM_TOKENIZER_FILE`/`LLM_TOKENIZER_URL`; их пишет `install.sh` по
  выбранному пресету — см. `install.sh --help`).
- `deploy/docker-compose.yml` — сервисы `llm-models-init`/`llm-model-init`/
  `llm`/`llm-gpu`, volume `llm-models`, монтирование
  `llm-models:/models/qwen:ro` в сервис `worker` (там же считаются токены
  чанкером — `QwenTokenCounter`).

## 2. Модели по уровням AI (ADR-004)

| Уровень | Модель | Квант | Файл (`LLM_MODEL_FILE`) | Источник GGUF (`LLM_MODEL_URL`) |
|---|---|---|---|---|
| `min` (пресет 3) | Qwen3.5-4B-Instruct | Q4_K_M ≈ 2,6 ГиБ | `qwen3.5-4b-instruct-q4_k_m.gguf` | `unsloth/Qwen3.5-4B-GGUF` |
| `medium` (пресет 4) | Qwen3.5-9B-Instruct | Q4_K_M ≈ 5,3 ГиБ | `qwen3.5-9b-instruct-q4_k_m.gguf` | `unsloth/Qwen3.5-9B-GGUF` |
| `max` (пресет 5) | Qwen3.5-35B-A3B-Instruct (MoE) | Q4_K_M ≈ 20,5 ГиБ | `qwen3.5-35b-a3b-instruct-q4_k_m.gguf` | `unsloth/Qwen3.5-35B-A3B-GGUF` |

`tokenizer.json` (переименован под `LLM_TOKENIZER_FILE` — нужен
`QwenTokenCounter`, `backend/core/summarization/tokens.py`) берётся из
официальных репозиториев `Qwen/Qwen3.5-<размер>` (публичные, без gate;
GGUF-репозитории `Qwen/…-GGUF` — gated, поэтому источник GGUF — публичное
зеркало `unsloth/…-GGUF`, файлы идентичны по содержимому квантизации).

Имена файлов на диске (`LLM_MODEL_FILE`/`LLM_TOKENIZER_FILE`) — КОНТРАКТ с
детектом доступности уровня AI (`backend/services/ai_levels.py` через
`backend/services/ai_tiers.py::TIERS[level].model_files`): именно эти пути
проверяются на «модель скачана» в админке.

Скачивание идемпотентно (проверка по наличию и минимальному размеру файла,
`LLM_MODEL_MIN_SIZE`) — повторный запуск на уже заполненном томе ничего не
перекачивает. Ручной запуск (прогреть volume заранее):

```bash
LLM_MODEL_FILE=qwen3.5-4b-instruct-q4_k_m.gguf \
LLM_MODEL_URL=https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf \
LLM_TOKENIZER_FILE=qwen3.5-4b-instruct.tokenizer.json \
LLM_TOKENIZER_URL=https://huggingface.co/Qwen/Qwen3.5-4B/resolve/main/tokenizer.json \
docker compose -f deploy/docker-compose.yml --env-file .env --profile llm run --rm llm-model-init
```

## 3. Запуск профиля

CPU (уровни `min`/`medium`, пресеты 3/4):

```bash
docker compose -f deploy/docker-compose.yml --env-file .env \
  --profile media --profile transcribe --profile llm up -d
```

GPU (уровень `max`, пресет 5 — GPU обязателен):

```bash
docker compose -f deploy/docker-compose.yml --env-file .env \
  --profile media --profile transcribe-gpu --profile llm-gpu up -d
```

Проверка готовности (порт `8080` — `llm`, `8081` — `llm-gpu` на хосте;
внутри docker-сети оба доступны как `llm:8080`/`llm-gpu:8080`, `llm-gpu`
также отвечает под алиасом `llm` — см. комментарий в `deploy/monitoring/prometheus.yml`):

```bash
curl http://localhost:8080/health
# пока модель грузится:  {"error":{"code":503,"message":"Loading model",...}}
# сервер готов:          {"status":"ok"}
```

## 4. Включение провайдера `qwen_local`

Дефолт репозитория в `config/plugins.yaml` — `summarizer.provider: "null"`
(безопасно для dev-окружений без LLM-сервера). Уровень `min` включается по
образцу закомментированного примера в `config/plugins.yaml`; уровни
`medium`/`max` собираются автоматически из `TIERS` (`backend/services/ai_tiers.py`)
при выборе уровня AI в админке — руками их прописывать не нужно (см.
`backend/services/instance_settings.py::_apply_tier_overrides`).

После правки `config/plugins.yaml` (уровень `min`, ручной dev-сценарий)
перезапустить `worker`:

```bash
docker compose -f deploy/docker-compose.yml --env-file .env up -d --force-recreate worker
```

## 5. Проверка вручную

1. Поднять профиль `llm`/`llm-gpu`, дождаться `docker compose ps` → healthy.
2. `curl http://localhost:8080/health` → `{"status":"ok"}`.
3. Включить уровень AI в админке (или `qwen_local` в `config/plugins.yaml`
   для ручного dev-сценария уровня `min`).
4. Прогнать пайплайн на тестовом сеансе (см. `docs/plugins/summarizer.md`)
   и убедиться, что `conference_sessions.summary_data` заполняется.

## 6. Ресурсы, thinking-режим и мониторинг

- Требования CPU/RAM/GPU по пресетам — `docs/architecture/adr/004-ai-tier-matrix.md`.
- `LLAMA_ARG_CTX_SIZE=16384` — с запасом на чанк до 8000 токенов + промпт +
  вывод (per-tier `max_tokens_map`/`max_tokens_reduce`, ADR-004).
- Thinking-режим (семейство Qwen3.5) отключается флагом `LLAMA_ARG_REASONING=off`
  (актуальная замена `--chat-template-kwargs '{"enable_thinking":false}'` из
  ADR-004 — та же семантика, но без деприкейшен-варнинга в логе на каждый
  запуск, проверено через find-docs по `common/arg.cpp` проекта llama.cpp).
- `/metrics` (`LLAMA_ARG_ENDPOINT_METRICS=1`) собирает Prometheus, профиль
  compose `monitoring` — `deploy/monitoring/prometheus.yml`, job `llm`.
