Как запустить AI-агента для ревью PR на своем VPS
Узнайте, как развернуть собственного AI-агента для анализа pull request на сервере. Инструкция по настройке фильтров, работе с diff и обеспечению безопасности данных вашего кода.
Что делает self-hosted агент для ревью PR
Self-hosted агент для ревью PR — это небольшая программа на вашем сервере. Она считывает diff запроса на слияние (PR) и отправляет только измененные строки в модель. Полученный ответ публикуется в виде встроенных комментариев к коду. Агент никогда не делает checkout вашей ветки и не читает файлы, которые не были затронуты в PR. Единственные учетные данные, которыми он владеет, — это один API-ключ модели и один токен, который может только оставлять комментарии и больше ничего. Это узкое определение агента, скорее напоминающее промпт с фильтрами, чем что-то автономное. Если вы хотите получить более полное представление перед тем, как создавать свой инструмент, поэтапный путь от концепций до цикла, который вы напишете сами охватывает все необходимые основы.
Модель может читать diff. Эта часть задачи решена. Важно то, куда попадает diff и у кого хранятся ключи. Использование стороннего сервиса для ревью означает, что каждый diff из каждого приватного репозитория покидает вашу сеть, попадает в логи третьей стороны и хранится согласно их политике удержания данных. На VPS (виртуальном частном сервере), который принадлежит вам, diff передается из GitHub на ваш сервер, а затем в API модели. Вы можете прочитать сорок строк кода, которые определяют, что именно будет отправлено.
Что нужно подготовить перед началом
- VPS с установленной Ubuntu 24.04 и уже зарегистрированным в репозитории self-hosted GitHub Actions runner. При регистрации добавьте ему дополнительную метку
pr-review, так как рабочий процесс ниже выбирает исполнителей именно по этой метке. - API-ключ Anthropic из консоли Claude.
- Репозиторий, в котором вы контролируете права на создание pull request. Частный репозиторий — наиболее простой вариант. Раздел о форках ниже описывает публичные репозитории, но решение там менее удобно.
Установка reviewer на VPS
Служба runner работает от имени непривилегированной учетной записи, которую вы создали при выполнении ./svc.sh install. Установите reviewer под той же учетной записью, чтобы задание могло выполнять его без sudo. Замените runner ниже на имя вашей учетной записи.
sudo apt update && sudo apt install -y gh python3-venv
sudo install -d -m 755 -o runner -g runner /opt/pr-review
sudo -u runner python3 -m venv /opt/pr-review/venv
sudo -u runner /opt/pr-review/venv/bin/pip install anthropic
gh --versiongh --version выводит gh version 2.45.0 в Ubuntu 24.04 по состоянию на август 2026 года. Любой релиз, начиная с 2.20, поддерживает флаг --input, используемый ниже. Сообщение Command 'gh' not found означает, что компонент universe не включен, поэтому выполните sudo add-apt-repository universe и попробуйте снова.
Где хранятся ключ и токен
Два секрета, два разных жизненных цикла. Ни один из них не должен попадать в репозиторий.
ANTHROPIC_API_KEY — это секрет репозитория, который задается в разделе Settings, затем Secrets and variables, а затем Actions. GitHub шифрует его и внедряет в окружение шага во время выполнения. Он никогда не существует в виде файла на диске и не попадает в историю git.
GITHUB_TOKEN работает иначе. Actions создает новый токен для каждого задания и уничтожает его после завершения работы. Права этого токена определяются блоком permissions: в рабочем процессе, поэтому именно здесь реализуется принцип наименьших привилегий:
permissions:
contents: read
pull-requests: writeЭтот токен может опубликовать рецензию. Он не может отправить коммит, объединить ветку, отредактировать файл рабочего процесса или получить доступ к другому репозиторию. Агент, который может оставлять комментарии, является рецензентом. Агент, который может выполнять push, является коммиттером, и на это никто не давал согласия. Относитесь к ключу модели с такой же осторожностью, поскольку он расходует средства вашего аккаунта. Подробнее об этой проблеме читайте в как ограничить доступ AI-агента к секретам.
Actions заменяет точную строку секрета на *** в логах задания. Замена происходит только при совпадении всей строки целиком, поэтому ключ, который вы закодировали в base64, разбили на две строки или вывели по одному символу, отобразится в открытом виде. Не добавляйте отладочный шаг, который выводит содержимое окружения.
Почему pull request из форка никогда не видит ваш API key
Правило GitHub простое: за исключением GITHUB_TOKEN, секреты не передаются в runner, если workflow запускается из форкнутого репозитория. Поэтому pull_request, запущенный из форка, начинает выполнение вашего скрипта без ANTHROPIC_API_KEY, и первый вызов API завершается ошибкой invalid x-api-key.
Заманчивое решение — переключить триггер на pull_request_target, который выполняется в контексте базового репозитория и получает доступ к секретам. Не делайте этого. В руководстве по безопасности GitHub прямо сказано, что такие workflow «являются привилегированными, что означает, что они используют тот же кэш основной ветки, что и другие привилегированные триггеры workflow, могут иметь права на запись в репозиторий и доступ к указанным секретам», и что этот результат «может быть использован для захвата репозитория».
То же руководство недвусмысленно высказывается о runner: «Self-hosted runners почти никогда не должны использоваться для публичных репозиториев на GitHub, так как любой пользователь может открыть pull request к репозиторию и скомпрометировать среду».
Это определяет два проектных решения. Задание содержит проверку, поэтому оно выполняется только для веток, отправленных в ваш собственный репозиторий. А workflow вообще не содержит шага actions/checkout. Агент никогда не сохраняет ветку на диск, поэтому враждебный pull request — это лишь текст, отправляемый в модель. Он не может запустить скрипт сборки на вашем VPS, так как на вашем VPS его никто не запускает. Однако текст не означает безопасность: diff, написанный незнакомцем, является недоверенным входным данным для модели. Это та же граница доверия, с которой вы сталкиваетесь, когда предоставляете агенту доступ к веб-поиску, и единственное, что ограничивает его здесь, — это то, что агент не может ничего, кроме публикации комментария.
Получение diff вместо репозитория
Один запрос позволяет получить весь diff в виде простого текста.
export GH_TOKEN=your_token # in the workflow this comes from secrets.GITHUB_TOKEN
gh api /repos/OWNER/REPO/pulls/42 -H "Accept: application/vnd.github.diff"Тип содержимого Accept: application/vnd.github.diff преобразует ответ из JSON-объекта, описывающего pull request, в сам unified diff, а gh api выводит тело ответа без изменений. Первая строка, которую вы увидите, должна начинаться с diff --git a/. Ошибка gh: Not Found (HTTP 404) означает, что токен не имеет доступа к репозиторию. В случае с fine-grained personal token это почти всегда означает, что было забыто разрешение Pull requests.
Фильтрация перед расходом токенов
Этот раздел определяет разницу между ботом, которого читают, и ботом, которого отключают. Каждый фильтр ниже срабатывает до того, как модель получит хотя бы один байт данных.
- Фильтры путей. Исключайте lock-файлы, сторонние библиотеки (vendored directories), минифицированные сборки и сгенерированный код. Комментарий модели к
package-lock.json— это чистый шум, а такие файлы часто составляют большую часть объема diff. - Ограничение по размеру. Если лимит превышен, пропустите проверку и завершите работу с кодом успеха. Рефакторинг на 4000 строк получит одну честную строку о том, что объем слишком велик для автоматической проверки, вместо шестидесяти догадок.
- Порог критичности и ограничение количества комментариев. Сообщайте о находках высокого и среднего уровня критичности, максимум десять штук, начиная с самых серьезных. Одиннадцатый комментарий никто не читает.
Скрипт
Сохраните это как /opt/pr-review/review.py. Скрипт считывает конфигурацию из переменных окружения, поэтому рабочий процесс позволяет менять модели без внесения изменений в код.
#!/usr/bin/env python3
"""Review only the changed lines of one pull request."""
import json
import os
import subprocess
import sys
import anthropic
REPO = os.environ["GITHUB_REPOSITORY"]
PR = os.environ["PR_NUMBER"]
MODEL = os.environ.get("REVIEW_MODEL", "claude-haiku-4-5-20251001")
MAX_DIFF_BYTES = int(os.environ.get("MAX_DIFF_BYTES", "120000"))
MIN_SEVERITY = os.environ.get("MIN_SEVERITY", "medium")
MAX_COMMENTS = 10
RANK = {"low": 0, "medium": 1, "high": 2}
SKIP = ("package-lock.json", "poetry.lock", "/vendor/", "/node_modules/", ".min.js")
raw_diff = subprocess.run(
["gh", "api", f"/repos/{REPO}/pulls/{PR}",
"-H", "Accept: application/vnd.github.diff"],
check=True, capture_output=True, text=True,
).stdoutРазделение diff по файлам делает возможной фильтрацию путей. Нумерация каждой строки позволяет привязывать комментарии к коду. GitHub принимает встроенный комментарий только для строки, которая входит в diff, поэтому модель должна указывать реальный номер строки. Передача номеров позволяет модели скопировать их, а не выдумывать.
def per_file(diff_text):
"""Split a unified diff into one string per file."""
sections, current = [], []
for line in diff_text.splitlines():
if line.startswith("diff --git ") and current:
sections.append("\n".join(current))
current = []
current.append(line)
if current:
sections.append("\n".join(current))
return sections
def annotate(section):
"""Prefix every line that exists in the new file with its line number."""
out, n, in_hunk = [], 0, False
for line in section.splitlines():
if line.startswith("@@"):
n = int(line.split("+")[1].split(",")[0].split(" ")[0])
in_hunk = True
out.append(line)
elif not in_hunk or line.startswith(("-", "\\")):
out.append(line)
else:
out.append(f"{n}\t{line}")
n += 1
return "\n".join(out)
kept = [s for s in per_file(raw_diff)
if not any(p in s.split("\n", 1)[0] for p in SKIP)]
payload = "\n".join(annotate(s) for s in kept)
if not payload.strip():
print("every changed file was filtered out")
raise SystemExit(0)
if len(payload) > MAX_DIFF_BYTES:
print(f"diff is {len(payload)} bytes, over the {MAX_DIFF_BYTES} cap")
raise SystemExit(0)Заголовок блока (hunk header) содержит нумерацию. @@ -12,7 +12,9 @@ указывает, что блок нового файла начинается со строки 12, поэтому счетчик начинает отсчет с этого значения и увеличивается только для добавленных и неизмененных строк. Удаленные строки проходят без нумерации, так как их не существует в новом файле. Условие для строк, начинающихся с обратной косой черты, пропускает маркер no-newline, который git записывает в конце файла; в противном случае этот маркер сдвигал бы каждый последующий номер на единицу.
Оба выхода используют статус 0, а не 1. Отфильтрованный или слишком большой pull request должен отображаться с зеленой галочкой. Красную галочку, на которую человек не может отреагировать, игнорируют, а если проигнорирована одна проверка, то будут проигнорированы и все остальные.
SYSTEM = (
"You review one pull request diff. Every line that exists in the new file is "
"prefixed with its line number and a tab character. "
"Report only defects you can see in the lines shown: a crash, a resource leak, "
"a security mistake, a wrong boundary condition, a broken contract with code "
"that is visible in this diff. Do not comment on style, naming or formatting. "
"Do not guess about code you cannot see. Leave out anything you are not "
"certain about. An empty findings list is a normal and common answer. "
'Reply with JSON only, in this shape: {"findings": [{"path": "src/app.py", '
'"line": 42, "severity": "high", "comment": "what is wrong, then why"}]} '
"Every line number must be one you can see in the left column of that file."
)
client = anthropic.Anthropic()
message = client.messages.create(
model=MODEL,
max_tokens=2000,
system=SYSTEM,
messages=[{"role": "user", "content": payload}],
)
print(f"stop={message.stop_reason} in={message.usage.input_tokens} "
f"out={message.usage.output_tokens}", file=sys.stderr)
text = message.content[0].text
findings = json.loads(text[text.find("{"):text.rfind("}") + 1])["findings"]
findings = [f for f in findings if RANK.get(f["severity"], 0) >= RANK[MIN_SEVERITY]]
findings.sort(key=lambda f: -RANK.get(f["severity"], 0))
del findings[MAX_COMMENTS:]
if not findings:
print("nothing above the severity threshold; posting no comment")
raise SystemExit(0)
review = {
"event": "COMMENT",
"body": f"Automated review of the changed lines. {len(findings)} finding(s).",
"comments": [
{"path": f["path"].removeprefix("b/"), "line": f["line"], "side": "RIGHT",
"body": f"**{f['severity']}** {f['comment']}"}
for f in findings
],
}
subprocess.run(
["gh", "api", "-X", "POST", f"/repos/{REPO}/pulls/{PR}/reviews", "--input", "-"],
input=json.dumps(review), text=True, check=True,
)Три детали в этом блоке являются критически важными. JSON извлекается между первым { и последним }, так как модель иногда оборачивает ответ в блок кода, а json.loads не может обработать такие границы. Из path удаляется начальный b/, так как этот префикс берется из заголовка diff, а GitHub ожидает путь относительно репозитория. А --input - отправляет весь обзор одним вызовом API, поэтому десять замечаний приходят как одно уведомление, а не как десять отдельных.
Если сообщать не о чем, скрипт ничего не публикует. Бот, который пишет «проблем не найдено» в каждом pull request, приучает людей игнорировать его сообщения, из-за чего они могут пропустить действительно важное уведомление.
Интеграция в рабочий процесс
Сохраните это как .github/workflows/pr-review.yml:
name: pr-review
on:
pull_request:
types: [opened, synchronize, reopened]
paths-ignore:
- '**.md'
- 'docs/**'
permissions:
contents: read
pull-requests: write
concurrency:
group: pr-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
review:
if: github.event.pull_request.head.repo.full_name == github.repository && !contains(github.event.pull_request.labels.*.name, 'no-ai-review')
runs-on: [self-hosted, linux, pr-review]
steps:
- name: Review the changed lines
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REVIEW_MODEL: claude-haiku-4-5-20251001
MIN_SEVERITY: medium
run: /opt/pr-review/venv/bin/python /opt/pr-review/review.pyGITHUB_REPOSITORY отсутствует в этом блоке env:, так как Actions устанавливает её для каждого задания автоматически. Группа concurrency важна для биллинга: без неё отправка трёх быстрых исправлений в ветку запустит три полных проверки, за которые придётся платить, а с ней — выполнится только последняя.
Строка if: выполняет две задачи. Первая часть пропускает pull requests из форков, которые в любом случае завершились бы ошибкой из-за отсутствия ключа. Вторая часть предоставляет команде выключатель: добавьте метку no-ai-review к pull request, и задание не запустится.
Откройте pull request и проследите за результатом:
gh run list --workflow=pr-review.yml --limit 3
gh run view --log
gh pr view 42 --commentsЗапуск, который завершается за несколько секунд с nothing above the severity threshold; posting no comment в логе, работает корректно. Для небольшого и чистого pull request это ожидаемый результат.
Сколько стоит автоматизированное ревью pull request?
Diff составляет почти весь объем входных данных, поэтому размер diff определяет цену. Ниже приведен расчет для diff объемом 500 строк вместе с системным промптом, выполненный с помощью эндпоинта для подсчета токенов, а не путем оценки.
Данные, на которых построен график
[
{
"label": "Haiku 4.5",
"input_tokens": "8,000",
"output_tokens": "1,200"
},
{
"label": "Sonnet 5",
"input_tokens": "10,400",
"output_tokens": "1,560"
},
{
"label": "Opus 5",
"input_tokens": "10,400",
"output_tokens": "1,560"
}
]Этот diff составил 8,000 входных токенов на Haiku 4.5 и 10,400 на Sonnet 5. Текст один и тот же, количество токенов разное. Модели Claude начиная с версии 4.7 используют новый токенизатор, который генерирует примерно на 30% больше токенов для того же объема входных данных, что задокументировано Anthropic на странице с ценами. Учитывайте это при сравнении стоимости за миллион токенов между новыми и старыми моделями.
Цены на октябрь 2026 года: Haiku 4.5 стоит $1 за миллион входных токенов и $5 за миллион выходных. Sonnet 5 — $2 и $10. Anthropic сначала продавала их по ознакомительной цене, затем сделала её стандартной и отменила запланированное повышение до $3 и $15. Opus 5 стоит $5 и $25.
Данные, на которых построен график
[
{
"label": "Haiku 4.5",
"cost_per_pr_cents": 1.4,
"cost_200_prs_usd": "2.80"
},
{
"label": "Sonnet 5",
"cost_per_pr_cents": 3.64,
"cost_200_prs_usd": "7.28"
},
{
"label": "Opus 5",
"cost_per_pr_cents": 9.1,
"cost_200_prs_usd": "18.20"
}
]Это 1.4 центов за pull request на Haiku 4.5 и 9.1 центов на Opus 5. Команда, объединяющая 200 pull request в месяц, платит около $2.80 на Haiku 4.5, $7.28 на Sonnet 5 или $18.20 на Opus 5.
Две вещи могут увеличить реальный счет сверх этой оценки. synchronize запускает ревью при каждом push, поэтому активная ветка с восемью push стоит как восемь ревью, а правило конкурентности помогает только тогда, когда push приходят почти одновременно. Цифры также предполагают, что фильтры путей работают корректно: один нефильтрованный lock-файл может сам по себе удвоить объем входных данных.
Кэширование промптов здесь не помогает. Кэшируемый префикс должен быть идентичен побайтово между вызовами, а diff каждый раз разный. Системный промпт — единственная стабильная часть, и он значительно меньше минимально допустимого размера для кэширования. Общие правила см. в когда кэширование промптов окупается, а по выбору между тремя моделями выше — какую модель Claude использовать для какой задачи.
Измерьте свои diff перед включением
Добавьте эту строку после того, как payload будет готов, затем запустите скрипт вручную для нескольких pull request за прошлый месяц:
print(client.messages.count_tokens(
model=MODEL, system=SYSTEM, messages=[{"role": "user", "content": payload}]
).input_tokens)Эндпоинт подсчета не запускает модель, поэтому он не потребляет входные или выходные токены и использует токенизатор, соответствующий указанной вами модели. Запустите его на десяти реальных pull request из вашего репозитория и возьмите медиану, а не среднее значение, чтобы одна масштабная миграция не исказила оценку.
Почему ботов для ревью отключают и как этого избежать
Две модели поведения разрушают доверие к таким ботам, и для обеих есть решение в коде выше.
Ревью всего сразу. Бот, который оставляет сорок комментариев, не получит ни одного прочтения. Порог критичности и ограничение в десять комментариев — это не вопрос вежливости, это способ сохранить видимость реальных замечаний. Сортировка по критичности перед обрезкой означает, что ограничение отсеет наименее важные замечания, а не случайные десять.
Уверенные комментарии о том, что бот не может проверить. Именно это заставляет инженеров навсегда отключать бота. Модели, которой показали 200 строк из 40 000 строк кодовой базы, все равно напишет: «это ломает инвалидацию кэша в redis_client.py» о файле, который она никогда не видела. Системный промпт должен прямо указывать: сообщайте только о дефектах, видимых в предоставленных строках, и опускайте все, в чем вы не уверены. Прямое указание на ошибку работает лучше, чем общая просьба о точности. Уведомление модели о том, что пустой результат — это нормально, предотвращает выдумывание комментариев к изменениям из двух строк. Дефекты, которые проявляются только в масштабах всего репозитория, — это другая задача, и self-hosting open-kritt для сканирования безопасности — один из способов решить её, не расширяя область видимости этого ревьюера.
Публикуйте ревью как COMMENT, никогда как REQUEST_CHANGES. Мнение модели не должно блокировать слияние. Как только это станет возможным, кто-то, работающий в сжатые сроки, удалит весь рабочий процесс, вместо того чтобы спорить с ним. Если вы хотите, чтобы суждение агента имело реальный вес, заставьте его создавать что-то, что вы можете проверить, а не то, во что вы должны верить. Это лежит в основе идеи получения от агента отчета с доказательствами, который вы можете перепроверить самостоятельно.
Типичные сбои и сообщения об ошибках
HTTP 422 при отправке рецензии. gh выводит gh: Unprocessable Entity (HTTP 422), а тело ответа указывает на конкретное поле: Pull request review thread line must be part of the diff. GitHub не может привязать такой комментарий. Обычно это происходит из-за номера строки, который модель выдумала сама, из-за path, содержащего префикс b/, или из-за комментария к удаленной строке, для которой нужно установить side в значение LEFT вместо RIGHT. Выведите JSON рецензии перед отправкой и вручную проверьте один комментарий на соответствие diff.
invalid x-api-key от API модели. Шаг завершается с ошибкой при первом вызове messages.create. Либо секрет ANTHROPIC_API_KEY не задан в репозитории, либо pull request пришел из форка, поэтому Actions не передали никаких секретов. Проверка форка в строке if: должна была пропустить этот случай, поэтому начните проверку с этой строки.
gh: Resource not accessible by integration (HTTP 403). Токен задания не имеет прав на запись в pull request. Добавьте pull-requests: write в блок permissions:. Если этот параметр уже добавлен, проверьте настройки Settings, затем Actions и General: там может быть установлена политика организации, ограничивающая права токенов рабочих процессов.
json.decoder.JSONDecodeError. Модель вернула JSON, который невозможно разобрать. Частая причина — ответ достиг лимита токенов и прервался на середине объекта. В журнале для этого случая выводится stop_reason: значение max_tokens означает, что нужно увеличить max_tokens или уменьшить MAX_COMMENTS.
Рабочий процесс не запускается. gh run list не показывает ничего для данного pull request. Убедитесь, что paths-ignore не отфильтровал все измененные файлы, затем проверьте условия для форка и меток, а также работоспособность исполнителя (runner) с помощью sudo systemctl status 'actions.runner.*' на VPS. Если исполнитель не в сети, задание остается в очереди без каких-либо сообщений об ошибках в pull request.
Все рецензии приходят пустыми. Установите MIN_SEVERITY в значение low для одного запуска. Если результаты появятся, значит, пороговое значение работает корректно. Если ничего не появилось, выведите payload и убедитесь, что фильтры не удалили весь diff целиком.
Запуск вместе с другими агентами
Reviewer занимает мало ресурсов, поэтому возникает соблазн разместить его на сервере, где уже запущено всё остальное. Если содержимое репозитория имеет значение, держите его отдельно. Этот процесс хранит токен, позволяющий комментировать код, и ключ, который расходует ваши средства. Self-hosted runner по своей сути является средой, где выполняется код из рабочих процессов. Выделенная непривилегированная учетная запись без прав sudo на хосте, где больше ничего не запущено, — это базовый стандарт безопасности. Если вы также используете интерактивных агентов, которые выполняют checkout кода, рекомендуемым шаблоном является использование отдельной виртуальной машины для каждого агента, а запуск агента для написания кода на VPS описывает общую настройку. Если Anthropic API для вас в новинку, создание первого приложения с Claude API на VPS будет более простым началом, чем данный проект.
FAQ
Нужен ли агенту для AI-ревью pull request доступ на запись к моему репозиторию?
Нет. Ему требуются pull-requests: write для публикации ревью и contents: read для получения diff. Это весь список прав; вы задаете их в блоке permissions: рабочего процесса, который ограничивает возможности GITHUB_TOKEN для конкретной задачи. С этими двумя разрешениями агент может оставлять комментарии к pull request, но не может отправлять коммиты или выполнять слияние веток. Используйте event: COMMENT вместо REQUEST_CHANGES для публикации ревью, чтобы агент также не мог блокировать слияние.
Почему мой комментарий к ревью завершается ошибкой HTTP 422?
GitHub принимает встроенный комментарий к ревью только для строки, которая входит в diff pull request, и возвращает Pull request review thread line must be part of the diff в противном случае. Убедитесь, что path указан относительно репозитория без префикса b/ из заголовка diff, и что номер строки находится внутри блока изменений этого файла. side должен быть RIGHT для добавленной или неизмененной строки и LEFT для удаленной. Добавление номера строки нового файла перед каждой строкой diff перед отправкой модели — это именно то, что предотвращает генерацию моделью несуществующих номеров.
Могу ли я запустить это в публичном репозитории с pull request из форков?
В рамках данной архитектуры — нет. GitHub не передает секреты в рабочий процесс, запущенный из форка, поэтому ключ модели отсутствует и выполнение завершается ошибкой. GitHub также указывает, что self-hosted раннеры «почти никогда не должны использоваться для публичных репозиториев», так как любой может открыть pull request, который приведет к выполнению кода на вашей машине. Для публичного проекта либо ограничьте ревью ветками, отправленными непосредственно в репозиторий (что делает проверка if:), либо перенесите этап ревью на GitHub-hosted раннер и примите тот факт, что diff покидает вашу инфраструктуру.
Какую модель использовать для ревью pull request?
Начните с Haiku 4.5. Чтение ограниченного diff в сравнении с фиксированным списком типов дефектов не является сложной задачей для логического вывода, а самая дешевая модель позволяет удерживать ежемесячные расходы на приемлемом уровне. Переходите на Sonnet 5, если заметите, что модель пропускает реальные ошибки в вашем языке или фреймворке; оценивайте это на основе измерений, а не предположений. Opus 5 значительно дороже двух других моделей в расчете на один pull request, что проще обосновать для релизной ветки, чем для каждого push в любую feature-ветку.