Як розгорнути AI-агента для перевірки PR на VPS
Запустіть self-hosted AI reviewer на VPS з Ubuntu 24.04: перевірка лише diff, фільтри шляхів і розміру, inline-коментарі та вартість одного PR.
Що робить self-hosted агент для перевірки PR
Self-hosted агент для перевірки PR — це невелика програма на сервері, яким ви керуєте. Вона читає diff pull request (PR) і надсилає моделі лише змінені рядки. Отримані результати публікуються як вбудовані коментарі до перевірки. Агент ніколи не перемикається на вашу гілку і не читає файли, яких pull request не змінював. Він зберігає лише один ключ API моделі та один токен, який дає змогу додавати коментарі й не дає жодних інших прав.
Модель уміє читати diff. Цю проблему вже вирішено. Важливо інше: куди потрапляє diff і хто зберігає ключ. Якщо використовується hosted бот для перевірки, кожен diff із кожного приватного репозиторію залишає вашу мережу, потрапляє до журналів сторонньої компанії та зберігається відповідно до її політики зберігання даних. На VPS (virtual private server), яким ви керуєте, diff проходить шлях від GitHub до вашого сервера, а потім до API моделі. Ви можете переглянути сорок рядків коду, які визначають, що саме буде надіслано.
Що потрібно підготувати
- VPS з Ubuntu 24.04 і вже зареєстрованим у репозиторії self-hosted runner для GitHub Actions. Під час реєстрації додайте йому додатковий label
pr-review, оскільки наведений нижче workflow вибирає runner за цим label. - API key Anthropic із Claude Console.
- Репозиторій, у якому ви контролюєте, хто може відкривати pull request. Приватний репозиторій — найпростіший варіант. Розділ про fork нижче охоплює публічний варіант, але відповідь там менш комфортна.
Встановіть reviewer на VPS
Сервіс runner працює від імені непривілейованого облікового запису, який ви створили під час виконання ./svc.sh install. Встановіть reviewer від імені цього самого облікового запису, щоб job міг запускати його без 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 станом на August 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 запускається з forked repository. Тому запуск pull_request з fork починає виконання скрипту без ANTHROPIC_API_KEY, і перший виклик API завершується помилкою invalid x-api-key.
Спокуса полягає в тому, щоб змінити trigger на pull_request_target. Він запускається в контексті base repository і отримує секрети. Тут так робити не можна. У власних рекомендаціях з безпеки GitHub зазначає, що такі workflow «мають привілейований доступ, тобто використовують той самий cache основної гілки з іншими привілейованими workflow trigger і можуть мати доступ на запис до repository та доступ до вказаних секретів». Також GitHub попереджає, що результат «можна використати для захоплення контролю над repository».
У тих самих рекомендаціях прямо описано ризик для runner: «Self-hosted runner майже ніколи не слід використовувати для публічних repository на GitHub, оскільки будь-який користувач може відкрити pull request до repository та скомпрометувати середовище».
Це визначає два рішення в дизайні. Job має guard, тому запускається лише для гілок, які передано до вашого власного repository. Workflow також взагалі не містить кроку actions/checkout. Agent ніколи не має цієї гілки на диску, тому hostile pull request є лише текстом, який передається моделі. Він не може виконати build script на вашому VPS, оскільки на вашому VPS нічого не запускає цей скрипт. Однак текст не є автоматично безпечним: diff, написаний сторонньою особою, є недовіреним input, який надходить до моделі. Це та сама межа довіри, з якою ви стикаєтеся, коли надаєте agent вебпошук, і єдине, що обмежує ризик тут, — agent може лише опублікувати comment.
Отримуйте 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 на сам уніфікований diff, а gh api виводить цей вміст без змін. Перший рядок має починатися з diff --git a/. gh: Not Found (HTTP 404) означає, що токен не має доступу до репозиторію. Для fine-grained personal token це майже завжди означає, що дозвіл Pull requests не було надано.
Фільтруйте до того, як витрачати токен
Цей розділ визначає, чи читатимуть люди повідомлення бота, чи вимкнуть його сповіщення. Кожен наведений нижче фільтр спрацьовує до того, як модель побачить хоча б один байт.
- Фільтри шляхів. Виключайте lock-файли, vendored-каталоги, мінімізовані бандли та згенерований код. Коментар моделі до
package-lock.json— це лише шум, а такі файли часто містять більшість байтів у diff. - Обмеження розміру. Якщо ліміт перевищено, пропускайте перевірку та завершуйте її зі статусом green. Для рефакторингу на 4,000 рядків краще повернути одне чесне повідомлення про те, що автоматично перевірити його було надто складно, ніж шістдесят здогадок.
- Поріг критичності та обмеження кількості коментарів. Повідомляйте про знахідки високої та середньої критичності, але не більше десяти, починаючи з найвищої критичності. Коментар одинадцять ніхто не читатиме.
Скрипт
Збережіть це у файлі /opt/pr-review/review.py. Скрипт читає конфігурацію зі змінних середовища, тому workflow може змінювати моделі без змін у коді.
#!/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 приймає inline-коментар лише для рядка, який входить до 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 містить нумерацію. @@ -12,7 +12,9 @@ означає, що hunk нового файла починається з рядка 12, тому лічильник починається з цього значення та збільшується лише для доданих і незмінених рядків. Видалені рядки проходять без номера, оскільки в новому файлі їх немає. Перевірка рядків, що починаються з backslash, пропускає маркер відсутнього переведення рядка, який 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 вирізається між першим { і останнім }, оскільки модель іноді обгортає відповідь у code fence, а json.loads не обробляє такий fence. Із 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 request із fork-репозиторіїв, які все одно завершилися б помилкою через відсутність ключа. Друга частина дає команді змогу вимкнути перевірку: додайте до pull request мітку no-ai-review, і завдання не запускатиметься.
Відкрийте 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 документує це на сторінці з цінами. Враховуйте це, коли порівнюєте новішу модель зі старішою лише за ціною за мільйон токенів.
Роздрібні ціни станом на August 2026: Haiku 4.5 коштує $1 за мільйон вхідних токенів і $5 за мільйон вихідних. Sonnet 5 коштує $2 і $10 за вступною ціною, чинною до 31 August 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 September 2026, помножте значення для Sonnet 5 на 1.5.
Два чинники збільшують фактичний рахунок порівняно з цією оцінкою. Тригер synchronize запускає перегляд після кожного push, тому активна гілка з вісьмома push коштує як вісім переглядів. Правило конкурентного виконання допомагає лише тоді, коли push відбуваються майже одночасно. Розрахунок також передбачає, що фільтри шляхів працюють: один lock file без фільтра може самостійно подвоїти обсяг вхідних даних.
Кешування промптів тут не допомагає. Кешований префікс має бути побайтно ідентичним у всіх викликах, а 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 із власного репозиторію та використайте медіану, а не середнє значення, щоб одна велика міграція не спотворила оцінку.
Чому review-ботів вимикають і як цього уникнути
Дві моделі поведінки руйнують довіру до таких ботів, і для обох у наведеному вище коді є виправлення.
Перевірка всього одразу. Бот, який залишає сорок коментарів, не отримує уваги до жодного з них. Поріг критичності та обмеження в десять коментарів — це не питання ввічливості. Вони дають змогу зберегти видимість справді важливих результатів. Сортування за критичністю перед скороченням списку означає, що обмеження відкидає найменш важливі результати, а не випадкові десять.
Впевнені коментарі щодо того, що бот не може перевірити. Саме через це інженери остаточно вимикають такі системи. Модель, якій показали 200 рядків із кодової бази на 40,000 рядків, все одно може написати: «це порушує інвалідацію кешу в redis_client.py» — про файл, якого вона ніколи не бачила. Системний prompt прямо задає обмеження: повідомляти лише про дефекти, видимі в показаних рядках, і не згадувати те, у чому немає впевненості. Пряме зазначення типу помилки працює краще, ніж загальне прохання бути точною. Повідомлення моделі, що порожній результат є нормальним, не дає їй вигадувати коментар для зміни на два рядки.
Публікуйте результат перевірки як COMMENT, а не як REQUEST_CHANGES. Думка моделі не повинна блокувати злиття змін. Щойно це стає можливим, хтось, кому потрібно дотриматися строків, просто видалить увесь workflow, а не витрачатиме час на суперечки з ним.
Типові помилки та відповідні повідомлення
HTTP 422 під час надсилання review. gh виводить gh: Unprocessable Entity (HTTP 422), а тіло відповіді містить назву поля: Pull request review thread line must be part of the diff. GitHub не може прив’язати цей коментар до diff. Найчастіші причини: номер рядка, вигаданий моделлю; path, у якому досі є префікс b/; або коментар до видаленого рядка, для якого потрібно встановити side у значення LEFT, а не RIGHT. Перед надсиланням виведіть JSON review і вручну перевірте один коментар за diff.
invalid x-api-key від API моделі. Крок завершується під час першого виклику messages.create. Або секрет ANTHROPIC_API_KEY не задано в репозиторії, або pull request надійшов із fork, тому Actions взагалі не передав секрети. Захисна перевірка fork у рядку if: мала пропустити цей крок, тому спочатку перевірте саме цей рядок.
gh: Resource not accessible by integration (HTTP 403). Токен job не може вносити зміни до pull request. Додайте pull-requests: write до блоку permissions:. Якщо цей параметр уже є, відкрийте Settings, потім Actions, потім General. Політика організації може обмежувати дозволи, які дозволено запитувати будь-якому токену workflow.
json.decoder.JSONDecodeError. Модель не повернула JSON, який можна розібрати. Найчастіша причина — відповідь досягла ліміту токенів і обірвалася всередині об’єкта. Рядок журналу виводить stop_reason саме для цього: значення max_tokens означає, що потрібно збільшити max_tokens або зменшити MAX_COMMENTS.
Workflow не запускається. gh run list не показує жодних даних для pull request. Перевірте, чи paths-ignore не відфільтрував усі змінені файли. Потім перевірте захисну перевірку fork і перевірку label. Після цього перевірте стан runner за допомогою sudo systemctl status 'actions.runner.*' на VPS. Офлайн runner залишає job у черзі, але в pull request не з’являється жодного повідомлення про помилку.
Усі review повертаються порожніми. Для одного запуску встановіть MIN_SEVERITY у значення low. Якщо результати з’являться, поріг працює правильно. Якщо результатів немає, виведіть payload і переконайтеся, що фільтри не вилучили весь diff.
Запуск поруч з іншими агентами
Рев’юер невеликий, тому може виникнути спокуса встановити його на сервері, де вже працює все інше. Якщо репозиторій має значення, тримайте його окремо. Цей процес має токен, за допомогою якого може коментувати ваш код, і ключ, який може витрачати ваші кошти. Self-hosted runner за задумом є місцем, де виконується код workflow. Окремий непривілейований обліковий запис без прав sudo на хості, де більше нічого не працює, — це базовий варіант. Якщо ви також запускаєте інтерактивних агентів, які отримують код із репозиторію, окрема disposable VM для кожного агента — це надійний підхід, а запуск coding agent на VPS описує загальне налаштування. Якщо Anthropic API для вас новий, перший застосунок Claude API на VPS — простіший варіант для початку, ніж цей.
FAQ
Чи потрібен агенту AI для перевірки PR доступ на запис до мого репозиторію?
Ні. Йому потрібен pull-requests: write, щоб опублікувати результат перевірки, і contents: read, щоб отримати diff. Це повний перелік дозволів. Ви задаєте його в блоці permissions: workflow, який обмежує можливості GITHUB_TOKEN для окремого завдання. З цими двома рядками агент може додавати коментарі до pull request, але не може відправляти commit або зливати гілку. Публікуйте результати перевірки за допомогою event: COMMENT, а не REQUEST_CHANGES, щоб агент також не міг заблокувати злиття.
Чому мій коментар до перевірки завершується помилкою HTTP 422?
GitHub приймає inline-коментар до перевірки лише для рядка, який входить до diff pull request. Якщо це не так, GitHub повертає Pull request review thread line must be part of the diff. Переконайтеся, що path вказує шлях відносно кореня репозиторію та не містить префікса b/ із заголовка diff, а номер рядка міститься всередині hunk цього файлу. side має бути RIGHT для доданого або незміненого рядка та LEFT для видаленого. Додавання номера рядка у новому файлі перед кожним рядком diff до передавання diff моделі запобігає вигадуванню номерів.
Чи можна запускати це в публічному репозиторії з pull request від fork?
За цієї схеми — ні. GitHub не передає секрети workflow, запущеному з fork, тому ключ моделі відсутній і запуск завершується помилкою. GitHub також зазначає, що self-hosted runners «майже ніколи не слід використовувати для публічних репозиторіїв», оскільки будь-хто може відкрити pull request, який спричинить виконання коду на вашій машині. Для публічного проєкту обмежте перевірку гілками, відправленими безпосередньо до репозиторію. Саме це робить умова if:. Або перенесіть етап перевірки на GitHub-hosted runner і прийміть, що diff залишатиме вашу інфраструктуру.
Яку модель використовувати для перевірки pull request?
Почніть із Haiku 4.5. Аналіз обмеженого diff за фіксованим переліком типів дефектів не потребує складного міркування. Найдешевша модель дає змогу утримувати щомісячні витрати на рівні, який не викликає заперечень. Перейдіть на Sonnet 5, якщо модель пропускає реальні помилки у вашій мові програмування або фреймворку. Оцінюйте це за результатами, а не робіть припущення заздалегідь. Opus 5 значно дорожча за кожен pull request порівняно з двома іншими моделями. Її легше обґрунтувати для release-гілки, ніж для кожного push у кожну feature-гілку.