команда · периметр

Облачные модели запрещены: ревью в закрытом контуре

Есть код, которому нельзя наружу. Если политика безопасности запрещает облачные модели или наружу просто нет пути, ревью может целиком идти на вашем железе. Продукт работает с OpenAI-совместимым API, поэтому Ollama или vLLM на вашем сервере — это настройка, а не форк.

Качество такого ревью задаёт модель, а не место, где она работает. Мы прогнали четыре открытые модели, от 122 миллиардов параметров до 2,8 триллиона, на диффе с четырьмя известными дефектами. В среднем они нашли от 1,7 до всех четырёх, а самая крупная находила все четыре в каждом прогоне. Облачная пара из остальных замеров раздела на том же диффе находит три.

Дальше — какую модель ставить и на каком железе, как настроить Ollama и vLLM, чтобы они не резали промпт и не ходили наружу, что показать безопаснику и почему периметр держит инсталляция, а не репозиторий.

Какую модель ставить

Замер 24 сентября 2026 года. Дифф — коммит с купоном в нашем учебном репозитории: 31 строка и четыре настоящих дефекта. Это дробные центы, почта клиента в строке лога, налог, посчитанный до скидки, и поиск кода купона в обычном объекте, где находятся и унаследованные свойства прототипа. Одна модель работает и генератором, и судьёй — в закрытом контуре обычно так и бывает. На каждую модель по три прогона.

МодельПараметры: всего / активныхЖелезо, примерноЗамечанийИз 4 дефектов
Qwen3.5-122B-A10B122B / 10B1–2 × H2002,72,3
DeepSeek-V4-Flash284B / 13B2 × H2002,01,7
DeepSeek-V4-Pro-08131,6T / 49B8 × H2003,32,7
Kimi K32,8T / 104B16 × H2005,34,0
для сравнения: claude-sonnet-5 + судья claude-opus-5—облако вендора3,73,0

Открытые модели мы запускали через OpenRouter: стойки с GPU у нас нет. Веса те же, что вы загрузите в vLLM, но провайдера OpenRouter выбирает сам, и часть провайдеров отдаёт квантованные веса — на полной точности цифры могут отличаться. Время и цена у хостинга свои, у вас они будут другими, поэтому в таблице их нет. Колонка «Железо» — прикидка по объёму весов с запасом под контекст. Строка облачной пары — замер 7 сентября на том же диффе из рецепта «Кто ищет, а кто судит».

Самая крупная открытая модель на этом диффе обошла облачную пару: Kimi K3 находил все четыре дефекта в каждом прогоне. DeepSeek-V4-Pro подошёл к паре вплотную, но поиск по прототипу не нашёл ни разу. Ложных замечаний в опубликованном мы не нашли: всё, что сверх четырёх известных дефектов, — по делу. Например, неизвестный купон молча даёт нулевую скидку, хотя в лог уходит «applied»; скидка и купон вместе могут увести сумму ниже нуля.

У открытых моделей номера строк съезжают: обычно на одну-три, у DeepSeek-V4-Flash — на двадцать и больше; у облачной пары они точные. Это не косметика. Судья проверяет находку по строке, которую она называет, и в двух прогонах из двенадцати снял верное замечание, потому что читал не тот код. В одном из них судья так и объяснил: строка 51 чистая, нарушение на 53-й, — а дефект был на 52-й.

Как подключить свою модель

Продукт не отличает облако от вашего сервера: Ollama, vLLM и другие серверы с OpenAI-совместимым API для него одинаковы. Но модели называйте явно. Без ролей в личном конфиге CLI возьмёт роли из .reviewgate/config.yml репозитория, и если там названа облачная модель, попросит её у вашего сервера — мы так и получили 404.

~/.config/reviewgate/config.yml
llm:
  provider: ollama
  base_url: http://llm.internal:11434/v1
  timeout_ms: 1800000          # 30 minutes; the default is 10
  generators:
    - model: qwen3.5:122b-a10b
  judges:
    - model: qwen3.5:122b-a10b

Если сервер — Ollama, два параметра демона обязательны:

  • OLLAMA_CONTEXT_LENGTH — размер контекста. Без него Ollama выбирает контекст сам, по объёму видеопамяти: на нашей машине вышло 4096 токенов, а промпт ревью даже для диффа в 31 строку весит около 7600. Лишнее Ollama отрезает молча — модель не видит ни правил, ни инструкций, а ревью всё равно проходит «успешно». Единственный след — строка truncating input prompt в логе демона.
  • OLLAMA_NO_CLOUD=1 выключает облачные функции. Без него демон держит соединение с ollama.com, а модели с тегом :cloud из его библиотеки работают на серверах Ollama, а не на ваших.
терминал
$ sudo systemctl edit ollama
[Service]
Environment="OLLAMA_CONTEXT_LENGTH=64000"
Environment="OLLAMA_NO_CLOUD=1"

$ sudo systemctl daemon-reload
$ sudo systemctl restart ollama

# after a review: no output means nothing was cut
$ journalctl -u ollama | grep "truncating input prompt"

Для бота то же самое, только переменными окружения. Роли в .reviewgate/config.yml репозитория главнее них: если там названа облачная модель, бот попросит её у вашего сервера, и ревью упадёт с ошибкой. Для репозиториев без своих ролей генератора задаёт LLM_MODEL, судью — LLM_JUDGES.

.env
LLM_PROVIDER=vllm
LLM_BASE_URL=http://llm.internal:8000/v1
LLM_MODEL=Qwen/Qwen3.5-122B-A10B
LLM_JUDGES=Qwen/Qwen3.5-122B-A10B
LLM_TIMEOUT_MS=1800000

Если сервер — vLLM, обрезки можно не бояться: слишком длинный промпт он не режет, а отвечает ошибкой, и ревью сообщает об этом в MR. Зато у него своя телеметрия: по умолчанию vLLM отправляет анонимную статистику — железо, архитектуру модели, настройки, без промптов. Выключает её VLLM_NO_USAGE_STATS=1 в окружении сервера vLLM.

Таймаут — не формальность. По умолчанию вызов модели ждёт 10 минут. В нашем замере судья Qwen3.5-122B в одном прогоне рассуждал 25 минут и выдал 131 тысячу токенов, а две попытки до этого оборвал таймаут — на 15 и на 30 минутах. Дайте вызовам запас и посмотрите, сколько идёт прогон, прежде чем ставить ревью на хук.

Безопаснику — показать, а не рассказать

Закрытый контур остаётся утверждением, пока кто-нибудь его не проверил. Ниже — полный список того, что получает модель во время ревью, и команды, которыми скептик сам посмотрит, куда уходят соединения.

Что получает модель, помимо инструкций самого продукта:

  • дифф изменения и пути файлов, исключённых из ревью (ignore или маркером в самом файле), — без их содержимого;
  • правила команды, её допущения из dont_flag и текст review_prompt — всё из .reviewgate/config.yml;
  • у бота — метаданные MR: заголовок, автор, ветки, проект, ссылка и описание до 2000 символов; CLI их не отправляет;
  • судья — целиком файлы, которых касаются находки, и определения, которые они импортируют; с full_file_context то же для изменённых файлов получает и генератор;
  • с environment_context — версии стека из ваших манифестов;
  • в режиме ответов — тред, на который отвечает бот, и строки кода вокруг обсуждаемой.
секреты из диффа не вырезаютсяЕсли ключ лежит в изменении, модель его видит: маскируем мы только то, что бот пишет в ответ. Поэтому периметр и важнее любого фильтра, который мы могли бы написать.

Чего модель не получает: историю git, рабочее дерево за пределами этих файлов и другие репозитории. А сам код после прогона продукт нигде не хранит.

И проверка. Запустите ревью из CLI со своей моделью и, пока оно идёт, посмотрите на сетевые соединения двух процессов: ревью и модели.

терминал
$ lsof -nP -i -a -p "$(pgrep -d, -x reviewgate)"
COMMAND     PID   FD  TYPE  NAME
reviewgat 84624  14u  IPv4  TCP 127.0.0.1:53958->127.0.0.1:11434 (ESTABLISHED)

$ lsof -nP -i -a -c ollama -c llama-server
COMMAND     PID   FD  TYPE  NAME
ollama    84616   3u  IPv4  TCP 127.0.0.1:11434 (LISTEN)
ollama    84616   7u  IPv4  TCP 127.0.0.1:11434->127.0.0.1:53958 (ESTABLISHED)
ollama    84616  10u  IPv4  TCP 127.0.0.1:54060->127.0.0.1:53960 (ESTABLISHED)
llama-ser 84745   3u  IPv4  TCP 127.0.0.1:53960 (LISTEN)
llama-ser 84745   4u  IPv4  TCP 127.0.0.1:53960->127.0.0.1:54060 (ESTABLISHED)

# the same daemon without OLLAMA_NO_CLOUD=1 adds one line (34.36.133.15 is ollama.com):
ollama    84446  11u  IPv4  TCP 10.8.1.1:53824->34.36.133.15:443 (ESTABLISHED)

Это живой вывод во время настоящего ревью на локальной модели: ревью ходит только на порт модели, Ollama — только к своему llama-server, и всё это не выходит за 127.0.0.1. Без OLLAMA_NO_CLOUD=1 та же команда показала ещё одну строку — соединение с ollama.com. Для бота в контейнере docker inspect покажет его сети и переменные окружения, то есть куда он настроен ходить, а tcpdump на хосте — куда он ходит на самом деле. Самая сильная проверка — allowlist исходящих, в котором просто нет строки для вендора моделей: если ревью работает с включённым allowlist, спор закончен.

Как устроить контур

Периметр держит инсталляция, а не репозиторий. Бот ревьюит MR по .reviewgate/config.yml из ветки этого же MR, а роль в этом файле может назвать любой бэкенд бота. Если названного бэкенда нет, основной генератор уходит к основному провайдеру инсталляции. Значит, если облако есть на инсталляции хоть где-то — именованным бэкендом или основным провайдером, — одна строка в MR отправит его дифф наружу, а сводка сообщит об этом уже после. Поэтому коду, которому нельзя наружу, нужна своя инсталляция без облака и allowlist исходящих без вендоров моделей. Делить по репозиториям на одной инсталляции можно только код, которому наружу можно.

Судьёй ставьте самую сильную модель, какая есть. Строка находки съезжает у генератора, а дальше решает судья: слабый читает не тот код и снимает верное замечание, сильный находит нужную строку сам. Судья DeepSeek-V4-Pro в таких случаях оставлял находку, а в одном прогоне прямо назвал настоящие номера строк. Если большой модели не хватает на каждый прогон целиком, пусть генератором работает модель поменьше, а большая остаётся судьёй.

И тратьте на модель, а не на проходы. Полноту двигает сама модель: дополнительные проходы судьи не добавят того, чего не увидел генератор, — судье позволено только отнимать. В таблице выше разница между самой слабой и самой сильной моделью — больше двух дефектов из четырёх.

Когда что-то пошло не так

Находки странные: про код, которого нет, на строках, где его нет. Скорее всего, Ollama обрезал промпт, и модель видела только его хвост — без правил и инструкций. Поищите в логе демона строку truncating input prompt и поднимите OLLAMA_CONTEXT_LENGTH.

Ревью падает с ошибкой 404 и именем облачной модели. Роли пришли из .reviewgate/config.yml репозитория, и там названа облачная модель. Для CLI задайте роли в личном конфиге, для бота поправьте роли в репозитории или уберите их, чтобы сработали LLM_MODEL и LLM_JUDGES.

Судья обрывается по таймауту, и находки публикуются без проверки. Прогон при этом не падает: судья выбывает, находки уходят без второго прохода, и сводка говорит об этом отдельной строкой. Модель отвечает дольше, чем ждёт вызов, — дайте timeout_ms (у бота — LLM_TIMEOUT_MS) больше запаса.

В lsof у Ollama есть соединение наружу. Не выставлен OLLAMA_NO_CLOUD=1, и демон держит соединение с ollama.com. Добавьте переменную в окружение сервиса и перезапустите демон.

Ревью сообщает, что контекст не помещается в окно модели. Промпт не влез в окно, которое vLLM задали через --max-model-len: такой промпт vLLM не режет, а отвечает ошибкой, и продукт пересказывает её с советами — убрать сгенерированные файлы через ignore, выключить full_file_context или разбить изменение. Можно и поднять само окно, если памяти на сервере хватает.

Судья снимает верные находки со ссылкой на номер строки. Причины снятия видны в блоке 🔬 при diagnostics: true и в отчёте reviewgate review --json. Строки съезжают у генератора, а судья слишком слаб, чтобы найти нужную сам. Поставьте судьёй модель сильнее — об этом глава выше.

Дальше