Как запустить AI-агента для ревью PR на своем VPS
Узнайте, как развернуть собственного AI-агента для анализа pull request на сервере. Инструкция по настройке фильтров, работе с diff и обеспечению безопасности данных вашего кода.
Что делает self-hosted агент для ревью PR
Self-hosted агент для проверки PR — это небольшая программа на принадлежащем вам сервере. Она читает diff pull request (PR) и отправляет модели только изменённые строки. Полученный результат публикуется как встроенные комментарии к проверке. Агент никогда не переключается на вашу ветку и не читает файлы, которых pull request не затрагивает. У него есть только один ключ API модели и один токен, который позволяет добавлять комментарии и не даёт никаких других прав. Это узкое определение агента. Такой агент ближе к prompt с предварительно настроенными фильтрами, чем к автономной системе. Если перед созданием такого агента вам нужна более полная картина, поэтапный путь от понятий до цикла, который вы напишете самостоятельно описывает необходимые основы.
Модель способна прочитать 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 на 500 строк вместе с системным промптом, подсчитанный через endpoint для подсчета токенов, а не оценочно.
The data behind this chart
[
{
"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 в рамках вводных цен, действующих до 31 августа 2026 года, после чего цена составит $3 и $15. Opus 5 стоит $5 и $25.
The data behind this chart
[
{
"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. С 1 сентября 2026 года умножайте показатели для Sonnet 5 на 1.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)Endpoint для подсчета не запускает модель, поэтому он не потребляет входные или выходные токены и использует токенизатор, соответствующий указанной модели. Запустите его на десяти реальных pull request из вашего репозитория и возьмите медиану, а не среднее значение, чтобы одна масштабная миграция не исказила оценку.
Почему ботов для ревью отключают и как этого избежать
Две модели поведения подрывают доверие к таким ботам, и для обеих в приведенном выше коде есть решение.
Ревью всего сразу. Бот, который оставляет сорок комментариев, не получает ни одного прочтения. Порог критичности и ограничение в десять комментариев — это не вопрос вежливости, а способ сохранить видимость реальных проблем. Сортировка по критичности перед обрезкой списка гарантирует, что ограничение отсеет наименее важные замечания, а не случайные десять.
Уверенные комментарии о том, что бот не может проверить. Именно это заставляет инженеров навсегда отключать бота. Модели, которой показали 200 строк из 40 000 строк кодовой базы, всё равно напишет «это нарушает инвалидацию кэша в redis_client.py» о файле, который она никогда не видела. Системный промпт ограничивает это простыми словами: сообщайте только о дефектах, видимых в предоставленных строках, и опускайте всё, в чём нет уверенности. Прямое указание на ошибку работает лучше, чем общие просьбы о точности, а сообщение модели о том, что пустой результат — это норма, предотвращает выдумывание комментариев к изменениям из двух строк.
Публикуйте ревью как 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-ветку.