SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-26

VPSでAI PRレビューエージェントを自分で運用する方法

Ubuntu 24.04のVPSにAI PRレビュアーを構築します。差分だけを送るプロンプト、パスとサイズ制限、インラインコメント、PRごとの費用まで具体的に解説します。

セルフホスト型 PR レビューエージェントの役割

自分で管理するサーバー上で動作する PR レビューエージェントは、小規模なプログラムです。プルリクエスト(PR)の diff を読み取り、変更された行だけをモデルに送信します。モデルの応答は、インラインのレビューコメントとして投稿されます。ブランチを checkout することはありません。プルリクエストで変更されていないファイルを読み取ることもありません。保持する認証情報は、モデル API(application programming interface)key 1 つと、コメント投稿以外の操作権限を持たない token 1 つだけです。これはエージェントの範囲を狭く定義したものです。自律的な仕組みというより、前段にフィルターを置いた prompt に近い構成です。この構成を作る前に全体像を確認したい場合は、概念から自分で書くループまでの段階的な手順で、その基礎を説明しています。

モデルは差分を読み取れます。この点は解決済みです。重要なのは、差分がどこへ送られ、誰がキーを保持するかです。ホスト型のレビュー bot では、すべてのプライベートリポジトリの差分がネットワークの外部へ送られ、第三者のログに記録され、その保持ポリシーの対象になります。自分で管理する VPS(virtual private server)では、差分は GitHub から自分のサーバーを経由してモデル API へ送られます。また、何を送信するかを決める40行のコードを自分で確認できます。

開始前に必要なもの

  • セルフホスト GitHub Actions runnerを登録済みの Ubuntu 24.04 VPS。登録時に追加のラベル pr-review を付けてください。下記のワークフローはこのラベルで runner を選択します。
  • Claude Console で取得した Anthropic API key。
  • pull request を作成できるユーザーを管理できるリポジトリ。private repository が最も簡単です。public repository の場合については下記の fork セクションで説明しますが、そこでの結論はあまり安心できるものではありません。

VPS に reviewer をインストールする

runner サービスは、./svc.sh install の実行時に作成した非特権アカウントで動作します。ジョブが sudo なしで reviewer を実行できるように、同じアカウントで reviewer をインストールします。以下の 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 は、2026 年 8 月時点の Ubuntu 24.04 では gh version 2.45.0 を出力します。2.20 以降のリリースでは、以下で使用する --input フラグを利用できます。Command 'gh' not found メッセージが表示される場合は universe コンポーネントが有効になっていないため、sudo add-apt-repository universe を実行してから再試行してください。

鍵とトークンの保管場所

2 つの Secret には、それぞれ異なる有効期間があります。どちらもリポジトリに保存しません。

ANTHROPIC_API_KEY はリポジトリ Secret です。Settings、Secrets and variables、Actions の順に開いて設定します。GitHub はこの値を暗号化し、実行時に step の環境へ注入します。ディスク上のファイルになることはなく、git の履歴にも残りません。

GITHUB_TOKEN の動作は異なります。Actions は job ごとに新しい token を発行し、job の終了時に破棄します。その token で実行できる操作は、workflow の permissions: ブロックで指定します。最小権限が実際に適用されるのはここです。

permissions:
  contents: read
  pull-requests: write

この token では review を投稿できます。ただし、commit の push、branch の merge、workflow ファイルの編集、別の repository へのアクセスはできません。comment を投稿できる agent は reviewer です。push できる agent は committer であり、その権限を与えることには誰も同意していません。model key にも同じ注意を払ってください。この key はアカウントの費用を発生させるためです。この問題の構成については、AI agent の到達範囲から Secret を外す方法 にも説明があります。

Actions は、job のログ内で Secret の正確な文字列を *** に置き換えます。ただし、完全に一致する文字列だけが対象です。そのため、key を base64 エンコードした場合、2 行に分割した場合、または 1 文字ずつ出力した場合は、平文で表示されます。環境変数をダンプする debug step を追加しないでください。

フォークからの pull request が API key を取得できない理由

GitHub のルールは単純です。GITHUB_TOKEN を除き、ワークフローがフォークされたリポジトリからトリガーされた場合、Secrets は runner に渡されません。そのため、フォークからの pull_request 実行では、スクリプトに ANTHROPIC_API_KEY がない状態で開始され、最初の API 呼び出しが invalid x-api-key で失敗します。

考えられる修正は、トリガーを pull_request_target に変更することです。これならベースリポジトリのコンテキストで実行され、Secrets も取得できます。ただし、ここではその方法を使わないでください。GitHub 自身のセキュリティガイダンスでは、これらのワークフローは「特権を持つため、他の特権ワークフロートリガーと main ブランチの同じ cache を共有し、リポジトリへの書き込み権限や参照先 Secrets へのアクセス権を持つ可能性がある」と説明されています。また、その結果は「リポジトリの乗っ取りに悪用される可能性がある」とされています。

同じガイダンスは runner についても明確です。「Self-hosted runner は GitHub 上の public repository では、ほとんど使用すべきではありません。誰でもそのリポジトリに対して pull request を開き、環境を侵害できるためです。」

このため、設計上の選択は 2 つあります。job にガードを設け、自分のリポジトリに push されたブランチでのみ実行します。また、ワークフローには actions/checkout step を一切含めません。agent がブランチをディスク上に置くことはないため、悪意のある pull request は model に送られるテキストにすぎません。VPS 上で build script を実行することもできません。VPS 上では何も実行されないためです。ただし、テキストが無害とは限りません。見知らぬ相手が作成した diff は model に届く信頼されていない入力であり、これは agent に Web 検索を実行させる ときと同じ信頼境界にあたります。ここで入力を封じ込めている唯一の要素は、この agent が comment の投稿しか実行できないことです。

差分を取得し、リポジトリ全体は取得しない

1 回のリクエストで、差分全体をプレーンテキストとして取得できます。

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 オブジェクトではなく、統合差分そのものになります。gh api は、その本文を変更せずに出力します。表示される最初の行は diff --git a/ で始まるはずです。gh: Not Found (HTTP 404) は、トークンからリポジトリを参照できないことを示します。fine-grained personal token では、ほとんどの場合、Pull requests 権限を有効にしていないことが原因です。

トークンを消費する前にフィルタリングする

このセクションは、人が読むボットと、ミュートされるボットを分けます。以下の各フィルタは、モデルが 1 バイトも受け取る前に実行されます。

  • パスフィルタ。 ロックファイル、ベンダー管理のディレクトリ、minified bundle、生成コードを除外します。package-lock.json に対するモデルのコメントは完全なノイズであり、これらのファイルが差分の大部分を占めることもよくあります。
  • サイズ上限。 上限を超えた場合はレビューをスキップし、成功として終了します。4,000 行のリファクタリングに対して、推測を 60 件並べるのではなく、自動レビューには大きすぎたことを正直に示す 1 行だけを返します。
  • 重大度のしきい値とコメント上限。 重大度が high または medium の指摘を、最大 10 件まで、重大度の高い順に報告します。11 件目のコメントを読む人はいません。

スクリプト

これを /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)

ハンクヘッダーに行番号が含まれています。@@ -12,7 +12,9 @@ は、新しいファイルのハンクが 12 行目から始まることを示しているため、カウンターもそこから開始し、追加行と変更されていない行だけで進みます。削除された行は新しいファイルには存在しないため、番号を付けずに処理します。バックスラッシュで始まる行に対するガードは、ファイル末尾に改行がないことを示すために git が出力するマーカーをスキップします。これを処理しないと、その後のすべての行番号が 1 つずれます。

どちらの終了処理も、1 ではなくステータス 0 を使用します。フィルタリングされた pull request やサイズ超過の pull request でも、成功として表示する必要があります。人が対処できない失敗表示は無視されます。1 つのチェックが無視されると、すべてのチェックが無視されるためです。

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,
)

このブロックには、処理上重要な点が 3 つあります。JSON は最初の { と最後の } の間で切り出します。モデルが回答を code fence で囲むことがあり、json.loads はその fence を処理できないためです。path から先頭の b/ を取り除きます。この接頭辞は diff ヘッダーに由来し、GitHub が必要とするのはリポジトリ相対パスだからです。また、--input - はレビュー全体を 1 回の API 呼び出しで送信します。そのため、10 件の指摘も 10 件の通知ではなく、1 件の通知として届きます。

報告する内容がない場合、スクリプトは何も投稿しません。すべての 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 は、Actions がすべてのジョブに設定するため、この env: ブロックには含まれていません。concurrency グループは料金に関係します。これがないと、ブランチに短時間で 3 件の修正を push した場合、完全なレビューが 3 回実行され、3 回分の料金が発生します。これがある場合は、最後の修正だけが実行対象として残ります。

if: の行には 2 つの役割があります。前半では fork からの pull request をスキップします。fork からの pull request は key がないため、いずれにしても失敗します。後半では、チームが実行を停止できるようにします。pull request に no-ai-review label を追加すると、ジョブは実行されません。

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 が表示され、数秒で完了する run であれば、正しく動作しています。小規模で変更内容が明確な pull request では、これが想定される結果です。

自動化した pull request レビューのコストはいくらですか?

入力のほとんどを diff が占めるため、diff のサイズで料金が決まります。以下は、500 行の diff と system prompt を実際に測定した例です。推定値ではなく、token counting 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 は、Haiku 4.5 では 8,000 input tokens、Sonnet 5 では 10,400 になりました。同じテキストでも、モデルによってカウントが異なります。4.7 以降の Claude モデルは新しい tokenizer を使用するため、同じ入力でもトークン数が約 30% 増えます。Anthropic はこの点を pricing page に記載しています。新しいモデルと古いモデルを、100 万トークンあたりの料金だけで比較する場合は、この差を考慮してください。

2026 年 8 月時点の通常料金は次のとおりです。Haiku 4.5 は input tokens が 100 万あたり $1、output が 100 万あたり $5 です。Sonnet 5 は、2026 年 8 月 31 日まで適用される導入価格では $2 と $10 で、その後は $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"
  }
]

pull request 1 件あたりの料金は、Haiku 4.5 で 1.4 セント、Opus 5 で 9.1 セントです。月に 200 件の pull request をマージするチームの場合、Haiku 4.5 では約 $2.80、Sonnet 5 では $7.28、Opus 5 では $18.20 かかります。2026 年 9 月 1 日以降は、Sonnet 5 の金額を 1.5 倍してください。

実際の請求額がこの見積もりを上回る主な要因は 2 つあります。synchronize の trigger は push のたびにレビューを実行するため、8 回 push された active branch では 8 回分のレビュー料金がかかります。ただし、concurrency rule が有効なのは、複数の push が短時間に集中した場合だけです。また、この数値は path filters が正しく機能していることを前提にしています。フィルター対象外の lock file が 1 つあるだけでも、入力が 2 倍になる可能性があります。

prompt caching は、この用途では効果がありません。キャッシュする prefix は呼び出し間で byte 単位で同一である必要がありますが、diff は毎回異なります。安定しているのは system prompt だけで、cacheable length の最小値を大幅に下回ります。一般的なルールについては prompt caching が元を取れる場合、上記 3 モデルから選ぶ方法については 用途ごとに使用する Claude モデル を参照してください。

有効化する前に自分の diff を測定する

payload を作成した後にこの行を追加し、先月の pull request をいくつか選んで script を手動で実行してください。

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

count endpoint はモデルを実行しないため、input tokens と output tokens を消費しません。また、指定したモデルに対応する tokenizer を使用します。自分の repository から実際の pull request を 10 件選んで実行し、平均値ではなく中央値を使用してください。大規模な migration が 1 件あるだけで、平均値が大きく変わる可能性があるためです。

レビュー bot が無効化される理由と、その回避方法

この bot への信頼を損なう動作は 2 つあります。どちらも、上記のコードで対策しています。

すべてを一度にレビューすること。 40 件のコメントを残す bot のコメントは、1 件も読まれません。重大度のしきい値とコメント数の上限 10 件は、配慮のためではありません。本当に重要な指摘を見える状態に保つための仕組みです。切り詰める前に重大度で並べ替えると、上限を超えたときに重要度の低い指摘から除外できます。ランダムに 10 件を残すことにはなりません。

確認できない内容を、確信があるようにコメントすること。 これが原因で、エンジニアは bot を二度と使わないようにします。40,000 行のコードベースのうち 200 行だけを提示されたモデルでも、見たことのないファイルについて「これは redis_client.py のキャッシュ無効化を壊します」と書くことがあります。システムプロンプトでは、これを明確な言葉で抑止しています。提示された行から確認できる不具合だけを報告し、確信が持てない内容は省略するよう指示します。一般的に正確さを求めるよりも、報告してはいけない失敗を直接指定するほうが効果的です。また、結果が空でも正常であるとモデルに伝えることで、2 行の変更について無理に何かを指摘する動作を防げます。

レビューは COMMENT として投稿し、REQUEST_CHANGES として投稿しないでください。モデルの意見によってマージをブロックできる状態にすべきではありません。ブロックできるようになった瞬間、締め切りに追われた誰かが bot と議論するのではなく、ワークフロー全体を削除してしまいます。

文字列で確認する障害パターン

レビューの投稿時に HTTP 422 が返る。 gh によって gh: Unprocessable Entity (HTTP 422) が出力され、レスポンス本文にはフィールド名が示されます: Pull request review thread line must be part of the diff。GitHub はそのコメントの位置を特定できません。よくある原因は、モデルが生成した行番号、b/ のプレフィックスが残っている path、または削除された行へのコメントです。削除された行の場合は、side を RIGHT ではなく LEFT に設定する必要があります。投稿前にレビュー JSON を出力し、手動で 1 件のコメントと diff を照合してください。

モデル API から invalid x-api-key が返る。 最初の messages.create 呼び出しでステップが失敗します。リポジトリに ANTHROPIC_API_KEY Secret が設定されていないか、プルリクエストが fork から作成されたため、Actions に Secret が一切渡されていない可能性があります。if: 行の fork ガードでスキップされるはずなので、まずその行を確認してください。

gh: Resource not accessible by integration (HTTP 403)。 ジョブトークンにプルリクエストを書き込む権限がありません。permissions: ブロックに pull-requests: write を追加してください。すでに設定されている場合は、Settings、次に Actions、次に General を確認してください。組織のポリシーによって、各ワークフロートークンが要求できる権限の上限が設定されている場合があります。

json.decoder.JSONDecodeError。 モデルが解析可能な JSON を返しませんでした。よくある原因は、トークン上限に達したレスポンスがオブジェクトの途中で終了したことです。この場合、ログ行に stop_reason が出力されます。max_tokens の値が示された場合は、max_tokens を増やすか MAX_COMMENTS を減らしてください。

ワークフローがまったく実行されない。 プルリクエストに対して gh run list に何も表示されません。まず paths-ignore が変更されたファイルをすべて除外していないか確認し、次に fork ガードとラベルガードを確認します。その後、VPS 上で sudo systemctl status 'actions.runner.*' を実行して runner が稼働しているか確認してください。runner がオフラインの場合、プルリクエスト内のどこにもエラーメッセージが表示されないまま、ジョブはキューに残ります。

すべてのレビューが空で返る。 1 回だけ MIN_SEVERITY を low に設定して実行してください。指摘が表示される場合は、しきい値が機能しています。何も表示されない場合は payload を出力し、フィルターによって diff 全体が除外されていないことを確認してください。

他のエージェントと並行して実行する

レビュアーは小規模なため、他のすべてを実行しているサーバーに追加したくなります。リポジトリが重要であれば、分離してください。このプロセスはコードにコメントできるトークンと、料金を支払うためのキーを保持します。また、self-hosted runner は設計上、ワークフローコードを実行する場所です。sudo 権限を持たない 専用の非特権アカウントを、他の処理を実行していないホスト上で使用する構成が最低限必要です。コードを checkout する対話型エージェントも実行する場合は、エージェントごとに使い捨ての VM を用意する構成が適しています。一般的なセットアップについては、VPS 上で coding agent を実行する方法を参照してください。Anthropic API を初めて使う場合は、この構成よりも、VPS 上で最初の Claude API アプリを作成するほうが小規模な開始点になります。

FAQ

AI PR レビューエージェントにはリポジトリへの書き込み権限が必要ですか?

いいえ。レビューを投稿するには pull-requests: write、差分を取得するには contents: read が必要です。必要なのはこれだけです。ワークフローの permissions: ブロックで設定すると、ジョブ単位の GITHUB_TOKEN で許可できる操作の上限になります。この2行があれば、エージェントは pull request にコメントできますが、コミットの push やブランチのマージはできません。レビューの投稿には REQUEST_CHANGES ではなく event: COMMENT を使用してください。これにより、マージをブロックすることもできません。

レビューコメントが HTTP 422 で失敗するのはなぜですか?

GitHub がインラインレビューコメントを受け付けるのは、pull request の差分に含まれる行だけです。含まれない行を指定すると Pull request review thread line must be part of the diff を返します。path がリポジトリ相対パスになっており、差分ヘッダーの b/ プレフィックスが付いていないことを確認してください。また、その行番号が対象ファイルの hunk 内にあることも確認します。追加行または変更されていない行では side を RIGHT にし、削除行では LEFT にする必要があります。モデルに渡す前に、差分の各行へ新しいファイル側の行番号を付けておけば、モデルが最初から行番号を推測することを防げます。

fork からの pull request がある公開リポジトリで実行できますか?

この設計では実行できません。GitHub は fork からトリガーされたワークフローに Secret を渡さないため、モデルのキーがなくなり、実行は失敗します。GitHub は、「公開リポジトリでは self-hosted runner をほぼ決して使用すべきではない」とも説明しています。誰でも pull request を作成し、あなたのマシン上でコードを実行させられるためです。公開プロジェクトでは、リポジトリ自体に push されたブランチだけにレビュアーを制限してください。これは if: ガードが行う制限です。または、レビュー処理を GitHub-hosted runner に移し、差分が自分のインフラストラクチャの外部へ出ることを受け入れてください。

pull request のレビューにはどのモデルを使用すべきですか?

まず Haiku 4.5 から始めてください。範囲を限定した差分を、固定した欠陥種別の一覧と照合する作業は、難しい推論問題ではありません。最も安価なモデルなら、月額費用を誰も問題にしない水準に抑えられます。使用している言語やフレームワークの実際のバグを見逃す場合は Sonnet 5 に上げてください。思い込みで判断せず、実際に測定します。3つの中では Opus 5 が pull request 単位で大幅に高価です。そのため、すべての機能ブランチへのすべての push で使うより、リリースブランチで使うほうが費用を正当化しやすくなります。