SSD Nodes Learn 🎉 VPS от $4.99/мес
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-07

Как запустить AI-агента для ревью PR на своем VPS

Разверните собственного AI-агента для автоматического анализа Pull Request на сервере. Узнайте, как настроить фильтры diff, ограничить доступ к коду и сократить расходы на API.

Что делает self-hosted агент для ревью PR

Self-hosted агент для ревью PR — это небольшая программа, работающая на вашем сервере. Она считывает diff запроса на слияние (PR) и отправляет только измененные строки в модель. Полученный ответ публикуется в виде inline-комментариев к коду. Агент не выполняет checkout вашей ветки и не читает файлы, которые не были затронуты в PR. Единственные учетные данные, которые он хранит — это один API-ключ модели и один токен, который имеет права только на публикацию комментариев и больше ни на что.

Модель способна прочитать diff. Эта задача решена. Важно то, куда отправляется diff и у кого хранятся ключи. Использование облачного бота для ревью означает, что diff из каждого вашего приватного репозитория покидает вашу сеть, попадает в логи третьей стороны и хранится согласно их политике удержания данных. На VPS, который вы контролируете, diff передается напрямую из GitHub на ваш сервер, а затем к API модели. Вы можете самостоятельно прочитать сорок строк кода, которые определяют, какая именно информация отправляется вовне.

Что потребуется перед началом работы

  • VPS с ОС Ubuntu 24.04, на котором уже настроен и зарегистрирован в репозитории self-hosted runner для GitHub Actions. При регистрации добавьте ему дополнительную метку 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 --version

gh --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: в файле workflow, поэтому именно здесь реализуется принцип минимальных привилегий:

permissions:
  contents: read
  pull-requests: write

Этот токен может опубликовать рецензию. Он не может отправить коммит, объединить ветку, изменить файл workflow или взаимодействовать с другим репозиторием. Агент, который может оставлять комментарии, является рецензентом. Агент, который может выполнять push, является коммиттером, а на это никто не давал согласия. Относитесь к ключу модели с такой же осторожностью, так как он расходует средства вашего аккаунта. Подробнее об этой проблеме читайте в как ограничить доступ AI-агента к секретам.

Actions заменяет точную строку секрета на *** в логах задания. Замена происходит только при совпадении всей строки целиком, поэтому ключ, который вы закодировали в base64, разбили на две строки или вывели по одному символу, отобразится в открытом виде. Не добавляйте шаги отладки, которые выводят содержимое переменных окружения.

Почему pull request из fork никогда не видит ваш API key

Правило GitHub простое: за исключением GITHUB_TOKEN, секреты не передаются в runner, если workflow запускается из fork-репозитория. Поэтому pull_request, запущенный из fork, начинает выполнение вашего скрипта без 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 в виде простого текста.

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 access token это почти всегда означает, что не было предоставлено разрешение Pull requests.

Фильтрация перед расходом токенов

Этот раздел определяет разницу между ботом, которого читают, и ботом, которого отключают. Каждый из приведенных ниже фильтров срабатывает до того, как модель получит хотя бы один байт данных.

  • Фильтры путей. Блокируйте файлы блокировок (lock files), сторонние библиотеки (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.py

GITHUB_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 для подсчета токенов, а не оценочно.

ChartTokens for one 500 line pull request diff, measured August 2026
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.

ChartCost of that one review at list prices, August 2026
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 requests в месяц, платит около $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 requests за прошлый месяц:

print(client.messages.count_tokens(
    model=MODEL, system=SYSTEM, messages=[{"role": "user", "content": payload}]
).input_tokens)

Endpoint для подсчета не запускает модель, поэтому он не потребляет входные или выходные токены и использует токенизатор, соответствующий указанной модели. Запустите его для десяти реальных pull requests из вашего репозитория и возьмите медианное значение, а не среднее, чтобы одна масштабная миграция не исказила оценку.

Почему боты для ревью получают мут и как этого избежать

Две модели поведения подрывают доверие к таким ботам, и для обеих в приведенном выше коде есть решение.

Ревью всего сразу. Бот, который оставляет сорок комментариев, не получает ни одного прочтения. Порог критичности и ограничение в десять комментариев — это не вопрос вежливости, а способ сохранить видимость реальных проблем. Сортировка по критичности перед обрезкой списка означает, что ограничение отсеет наименее важные замечания, а не случайные десять.

Уверенные комментарии о том, что бот не может проверить. Именно это заставляет инженеров навсегда отключать бота. Модели, которой показали 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 по своей архитектуре предназначен для выполнения кода из рабочих процессов (workflows). Выделенная непривилегированная учетная запись без прав sudo на хосте, где больше ничего не запущено — это базовый стандарт безопасности. Если вы также используете интерактивных агентов, которые выполняют checkout кода, используйте схему с одноразовой виртуальной машиной для каждого агента, а запуск агента для написания кода на VPS описывает общую настройку. Если Anthropic API для вас в новинку, первое приложение с Claude API на VPS станет более подходящей отправной точкой, чем этот проект.

FAQ

Нужен ли агенту для AI-ревью кода доступ на запись в мой репозиторий?

Нет. Ему требуются 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-ветку.