100 lines
5.8 KiB
Bash
Executable File
100 lines
5.8 KiB
Bash
Executable File
#!/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}"
|