Files
vidconf/deploy/llm/download-model.sh

100 lines
5.8 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
# Идемпотентное скачивание GGUF-модели для локального LLM-сервера
# (llama.cpp), см. ADR-004 (`docs/architecture/adr/004-ai-tier-matrix.md`).
#
# Генерализовано под 3 уровня AI (min/medium/max, разные модели семейства
# Qwen3.5) — конкретную модель задают переменные окружения, которые пишет
# `install.sh` при выборе пресета 3/4/5 (см. `docs/deploy/llm-setup.md`):
# LLM_MODEL_FILE — имя файла модели внутри $MODEL_DIR (ПОД ЭТИМ именем
# сохраняется локально — не обязано совпадать с
# именем файла в источнике, см. GGUF_URL/GGUF_FILE_NAME)
# LLM_MODEL_URL — прямая ссылка на GGUF (resolve/main/...)
# LLM_MODEL_MIN_SIZE — ожидаемый минимальный размер файла в байтах
# (страховка от оборванной/битой докачки)
# LLM_TOKENIZER_FILE — имя файла токенизатора внутри $MODEL_DIR
# LLM_TOKENIZER_URL — ссылка на tokenizer.json базовой модели (нужен
# QwenTokenCounter, backend/core/summarization/tokens.py)
#
# Имена файлов (LLM_MODEL_FILE/LLM_TOKENIZER_FILE) — КОНТРАКТ с
# `backend/services/ai_tiers.py` (см. докстринг модуля): детект доступности
# уровня AI и сам плагин `qwen_local` (`tokenizer_path`/`model` в
# `config/plugins.yaml`) ищут файлы по путям `{QWEN_MODELS_ROOT}/{model}.gguf`
# и `{QWEN_MODELS_ROOT}/{model}.tokenizer.json` из этого модуля — install.sh
# обязан передавать сюда ИМЕННО эти имена, не имена файлов в источнике.
#
# Дефолты ниже соответствуют уровню `min` (Qwen3.5-4B-Instruct, ADR-004) —
# скрипт можно запускать вручную без install.sh (см. `docs/deploy/llm-setup.md`).
#
# Запускается init-контейнером `llm-model-init` (образ curlimages/curl,
# профили compose `llm`/`llm-gpu`) перед стартом сервиса `llm`/`llm-gpu`.
#
# Идемпотентность: перед скачиванием каждого файла проверяется его
# наличие и размер (не меньше ожидаемого минимума) — повторный запуск
# на уже заполненном томе (в т.ч. после смены пресета на модель ДРУГОГО
# уровня, ранее уже скачанную в этот же volume) ничего не перекачивает.
set -eu
MODEL_DIR="${MODEL_DIR:-/models/qwen}"
GGUF_URL="${LLM_MODEL_URL:-https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf}"
GGUF_FILE_NAME="${LLM_MODEL_FILE:-qwen3.5-4b-instruct-q4_k_m.gguf}"
GGUF_FILE="${MODEL_DIR}/${GGUF_FILE_NAME}"
# Порог занижен относительно реального размера файла — не ловить ложные
# срабатывания на разных ревизиях модели, но отсекать битые/оборванные
# докачки (пустой или сильно урезанный файл). install.sh передаёт более
# точный порог для выбранного уровня.
GGUF_MIN_SIZE="${LLM_MODEL_MIN_SIZE:-2000000000}"
TOKENIZER_URL="${LLM_TOKENIZER_URL:-https://huggingface.co/Qwen/Qwen3.5-4B/resolve/main/tokenizer.json}"
TOKENIZER_FILE_NAME="${LLM_TOKENIZER_FILE:-qwen3.5-4b-instruct.tokenizer.json}"
TOKENIZER_FILE="${MODEL_DIR}/${TOKENIZER_FILE_NAME}"
# tokenizer.json семейства Qwen3.5 занимает ~12 МБ (словарь + merges) —
# порог просто отсекает пустой/HTML-ответ вместо файла.
TOKENIZER_MIN_SIZE=$((1 * 1000 * 1000))
# Размер существующего файла в байтах (0, если файла нет).
file_size() {
if [ -f "$1" ]; then
wc -c < "$1" | tr -d ' '
else
echo 0
fi
}
# Скачать файл по URL, если он отсутствует или меньше минимального размера.
download_if_missing() {
url="$1"
dest="$2"
min_size="$3"
name="$4"
size=$(file_size "$dest")
if [ "$size" -ge "$min_size" ]; then
echo "[download-model] ${name}: уже скачан (${dest}, ${size} байт) — пропуск"
return 0
fi
echo "[download-model] ${name}: скачивание из ${url} в ${dest}"
tmp="${dest}.part"
# -f — считать HTTP-ошибки (4xx/5xx) провалом; -L — следовать
# редиректам HuggingFace на CDN; --retry — устойчивость к обрывам сети.
curl -fL --retry 5 --retry-delay 5 -o "$tmp" "$url"
new_size=$(file_size "$tmp")
if [ "$new_size" -lt "$min_size" ]; then
echo "[download-model] ${name}: скачанный файл подозрительно мал (${new_size} байт) — прерывание" >&2
rm -f "$tmp"
exit 1
fi
mv "$tmp" "$dest"
echo "[download-model] ${name}: готово (${dest}, ${new_size} байт)"
}
mkdir -p "$MODEL_DIR"
download_if_missing "$GGUF_URL" "$GGUF_FILE" "$GGUF_MIN_SIZE" "GGUF-модель (${GGUF_FILE_NAME})"
download_if_missing "$TOKENIZER_URL" "$TOKENIZER_FILE" "$TOKENIZER_MIN_SIZE" "${TOKENIZER_FILE_NAME}"
echo "[download-model] всё готово: ${MODEL_DIR}"