SSD Nodes Learn Hosting plans →
Guias Matt ConnorPor Matt Connor · Atualizado 2026-08-26

Como hospedar um agente de revisão de PR em uma VPS

Execute um revisor de pull requests em uma VPS própria, com runner auto-hospedado, prompts só para o diff, filtros, comentários inline e custo por PR.

O que um agente de revisão de PR auto-hospedado faz

Um agente de revisão de PR autoalojado é um programa pequeno num servidor que controla. Lê o diff de um pull request (PR) e envia para um modelo apenas as linhas alteradas. O resultado é publicado como comentários inline na revisão. Nunca faz checkout da sua branch e nunca lê um ficheiro que o pull request não tenha alterado. As únicas credenciais que possui são uma chave de API (application programming interface) de um modelo e um token que pode publicar comentários, sem qualquer outra permissão. Esta é uma definição restrita de agente, mais próxima de um prompt com filtros à sua frente do que de algo autónomo. Se quiser compreender o contexto completo antes de criar este agente, o percurso faseado dos conceitos até um loop escrito por si aborda os fundamentos.

Um modelo consegue ler um diff. Essa parte está resolvida. O que importa é para onde o diff vai e quem detém a chave. Um bot de revisão alojado significa que todos os diffs de todos os repositórios privados saem da sua rede, entram nos logs de terceiros e ficam sujeitos à política de retenção desse terceiro. Num VPS (servidor privado virtual) que lhe pertence, o diff vai do GitHub para o seu servidor e daí para a API do modelo. Também pode ler as quarenta linhas de código que determinam o que é enviado.

O que precisa de ter antes de começar

  • Uma VPS com Ubuntu 24.04 e um runner self-hosted do GitHub Actions já registado no repositório. Atribua-lhe a etiqueta adicional pr-review durante o registo, porque o workflow abaixo seleciona essa etiqueta.
  • Uma chave de API da Anthropic obtida na Claude Console.
  • Um repositório onde possa controlar quem abre um pull request. Um repositório privado é o caso mais simples. A secção seguinte sobre forks aborda o caso público, mas a resposta é menos confortável.

Instale o reviewer na VPS

O serviço runner é executado com a conta sem privilégios que criou ao executar ./svc.sh install. Instale o reviewer nessa mesma conta para que o job possa executá-lo sem sudo. Substitua runner abaixo pelo nome da sua conta.

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 imprime gh version 2.45.0 no Ubuntu 24.04 em agosto de 2026. Qualquer versão a partir de 2.20 inclui a flag --input usada abaixo. Uma mensagem Command 'gh' not found significa que o componente universe não está ativado. Nesse caso, execute sudo add-apt-repository universe e tente novamente.

Onde ficam a chave e o token

Dois segredos, dois períodos de validade diferentes. Nenhum dos dois deve ficar no repositório.

ANTHROPIC_API_KEY é um segredo do repositório, definido em Settings, depois Secrets and variables e depois Actions. O GitHub cifra o segredo e injeta-o no ambiente do passo durante a execução. Nunca é um ficheiro no disco e nunca fica no histórico do git.

GITHUB_TOKEN funciona de forma diferente. O Actions cria um token novo para cada job e destrói-o quando o job termina. As operações que esse token pode executar são definidas pelo bloco permissions: no workflow. É aqui que o princípio do menor privilégio é efetivamente aplicado:

permissions:
  contents: read
  pull-requests: write

Esse token pode publicar uma revisão. Não pode enviar um commit, fazer merge de uma branch, editar um ficheiro de workflow nem aceder a outro repositório. Um agente que pode comentar é um revisor. Um agente que pode fazer push é um committer, e ninguém autorizou isso. Trate a chave do modelo com o mesmo cuidado, porque ela pode gastar dinheiro da sua conta. Há mais informações sobre este tipo de problema em manter os segredos fora do alcance de um agente de IA.

O Actions substitui a cadeia exata do segredo por *** nos logs dos jobs. A correspondência é feita apenas com a cadeia exata. Por isso, uma chave codificada em base64, dividida em duas linhas ou impressa um caractere de cada vez aparece em texto simples. Não adicione um passo de depuração que descarregue o ambiente.

Por que um pull request de um fork nunca vê a sua chave de API

A regra do GitHub é simples: com exceção de GITHUB_TOKEN, os secrets não são enviados para o runner quando um workflow é acionado a partir de um repositório fork. Assim, uma execução de pull_request iniciada por um fork começa o seu script sem ANTHROPIC_API_KEY, e a primeira chamada à API falha com invalid x-api-key.

A correção tentadora é mudar o acionador para pull_request_target, que executa no contexto do repositório base e também recebe os secrets. Não faça isso neste caso. As próprias orientações de segurança do GitHub dizem que esses workflows "são privilegiados, o que significa que partilham a mesma cache do branch principal com outros acionadores de workflows privilegiados e podem ter acesso de escrita ao repositório e acesso aos secrets referenciados", e que o resultado "pode ser explorado para assumir o controlo de um repositório".

As mesmas orientações são claras sobre o runner: "Os self-hosted runners quase nunca devem ser usados para repositórios públicos no GitHub, porque qualquer utilizador pode abrir pull requests contra o repositório e comprometer o ambiente."

Isso determina duas escolhas de projeto. O job inclui uma condição para ser executado apenas em branches enviados para o seu próprio repositório. E o workflow não tem nenhum passo actions/checkout. O agente nunca tem o branch no disco, portanto um pull request hostil é apenas texto enviado para um modelo. Ele não pode executar um script de build no seu VPS, porque nada no seu VPS chega a executá-lo. Texto não é sinónimo de conteúdo inofensivo: um diff escrito por um desconhecido é uma entrada não confiável que chega a um modelo, dentro do mesmo limite de confiança que encontra quando dá a um agente acesso à pesquisa na Web, e a única contenção existente aqui é que este agente só pode publicar um comentário.

Obtenha o diff, não o repositório

Uma única solicitação obtém todo o diff como texto simples.

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"

O tipo de mídia Accept: application/vnd.github.diff transforma a resposta de um objeto JSON que descreve o pull request no próprio diff unificado, e gh api imprime esse corpo sem alterações. A primeira linha apresentada deve começar com diff --git a/. Um gh: Not Found (HTTP 404) significa que o token não consegue ver o repositório. Num token pessoal com permissões granulares, isso quase sempre significa que a permissão Pull requests não foi ativada.

Filtre antes de gastar um token

Esta secção é o que distingue um bot que as pessoas leem de um bot que silenciam. Cada filtro abaixo é executado antes de o modelo ver um único byte.

  • Filtros de caminho. Ignore ficheiros de bloqueio, diretórios de dependências incluídas, bundles minificados e código gerado. Um comentário do modelo sobre package-lock.json é puro ruído, e esses ficheiros costumam representar a maior parte dos bytes de um diff.
  • Um limite de tamanho. Acima do limite, ignore a revisão e termine com sucesso. Uma refatoração com 4,000 linhas recebe uma única mensagem clara a indicar que era demasiado grande para ser revista automaticamente, em vez de sessenta suposições.
  • Um limiar de gravidade e um limite de comentários. Comunique os problemas de gravidade alta e média, até um máximo de dez, começando pelos mais graves. Ninguém lê o comentário onze.

O script

Guarde-o como /opt/pr-review/review.py. Ele lê a configuração a partir do ambiente, por isso o workflow pode mudar de modelo sem alterar o código.

#!/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

Dividir o diff por ficheiro é o que permite filtrar por caminho. Numerar cada linha é o que faz os comentários da revisão serem colocados corretamente. O GitHub aceita um comentário inline apenas numa linha que faça parte do diff, por isso o modelo tem de indicar um número de linha real. Fornecer os números permite-lhe copiar um, em vez de inventar um.

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)

O cabeçalho do hunk contém a numeração. @@ -12,7 +12,9 @@ indica que o hunk do ficheiro novo começa na linha 12, por isso o contador começa aí e avança apenas nas linhas adicionadas e inalteradas. As linhas removidas passam sem numeração, porque não existem no ficheiro novo. A verificação das linhas que começam com uma barra invertida ignora o marcador de ausência de nova linha que o git escreve no fim de um ficheiro. Sem essa verificação, todos os números seguintes ficariam deslocados uma posição.

Ambas as saídas usam o estado 0, não 1. Um pull request filtrado ou demasiado grande deve apresentar uma verificação verde. Uma verificação vermelha sobre a qual uma pessoa não pode agir é ignorada. Depois de uma verificação ser ignorada, todas passam a ser.

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

Há três detalhes essenciais nesse bloco. O JSON é extraído entre o primeiro { e o último } porque, por vezes, um modelo envolve a resposta num code fence, e json.loads não aceita esse fence. O path é removido de um b/ inicial, porque esse prefixo vem do cabeçalho do diff e o GitHub precisa de um caminho relativo ao repositório. E --input - envia toda a revisão numa única chamada à API, para que dez problemas cheguem como uma notificação, e não como dez.

Quando não há nada a reportar, o script não publica nada. Um bot que escreve "não foram encontrados problemas" em todos os pull requests ensina as pessoas a passar rapidamente por essa mensagem. Depois, passam rapidamente também pelo pull request que era importante.

Ligue-o ao fluxo de trabalho

Guarde isto como .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 não está nesse bloco env: porque o Actions já o define para todas as tarefas. O grupo concurrency é importante para a faturação: sem ele, enviar três correções rápidas para um branch inicia três revisões completas e paga pelas três; com ele, apenas a última permanece.

A linha if: tem duas funções. A primeira metade ignora pull requests de forks, que falhariam de qualquer forma sem uma key. A segunda metade fornece à equipa um interruptor: adicione a etiqueta no-ai-review a um pull request e a tarefa não será executada.

Abra um pull request e observe o que acontece:

gh run list --workflow=pr-review.yml --limit 3
gh run view --log
gh pr view 42 --comments

Uma execução que termina em poucos segundos com nothing above the severity threshold; posting no comment no log está a funcionar corretamente. Num pull request pequeno e limpo, esse é o resultado esperado.

Quanto custa uma revisão automatizada de pull request?

O diff representa quase toda a entrada, portanto o tamanho do diff define o preço. Abaixo está um diff medido com 500 linhas, juntamente com o system prompt. A contagem foi feita com o endpoint de contagem de tokens, e não por estimativa.

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"
  }
]

Esse diff correspondeu a 8,000 tokens de entrada no Haiku 4.5 e a 10,400 no Sonnet 5. O texto é o mesmo, mas a contagem é diferente. Os modelos Claude a partir do 4.7 usam um tokenizador mais recente, que produz aproximadamente 30% mais tokens para a mesma entrada, conforme documentado pela Anthropic na sua página de preços. Tenha isso em conta ao comparar o preço por milhão de tokens entre um modelo mais recente e um modelo mais antigo.

Preços de tabela em agosto de 2026: o Haiku 4.5 custa $1 por milhão de tokens de entrada e $5 por milhão de tokens de saída. O Sonnet 5 custa $2 e $10 durante o preço introdutório válido até 31 August 2026; depois, passa a custar $3 e $15. O Opus 5 custa $5 e $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"
  }
]

Isso corresponde a 1.4 centavos por pull request no Haiku 4.5 e 9.1 centavos no Opus 5. Uma equipa que faça merge de 200 pull requests por mês paga aproximadamente $2.80 no Haiku 4.5, $7.28 no Sonnet 5 ou $18.20 no Opus 5. A partir de 1 September 2026, multiplique o valor do Sonnet 5 por 1.5.

Dois fatores fazem com que a fatura real fique acima dessa estimativa. O acionador synchronize faz uma revisão a cada push. Portanto, uma branch ativa com oito pushes custa oito revisões. A regra de concorrência só ajuda quando os pushes ocorrem com pouca diferença de tempo. Os valores também pressupõem que os filtros de caminho estão a funcionar: um único ficheiro de lock sem filtro pode duplicar a entrada.

O prompt caching não ajuda neste caso. O prefixo em cache tem de ser byte a byte idêntico entre as chamadas, e o diff muda sempre. O system prompt é a única parte estável, mas fica muito abaixo do comprimento mínimo necessário para o cache. Para a regra geral, consulte quando o prompt caching compensa, e para escolher entre os três modelos acima, consulte qual modelo Claude usar em cada tarefa.

Meça os seus próprios diffs antes de ativar isto

Adicione esta linha depois de criar payload e execute manualmente o script em alguns pull requests do mês passado:

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

O endpoint de contagem não executa o modelo. Portanto, não consome tokens de entrada nem de saída. Também usa o tokenizador correspondente ao modelo indicado. Execute-o em dez pull requests reais do seu próprio repositório e use a mediana em vez da média, para que uma migração muito grande não distorça a estimativa.

Por que os bots de revisão são silenciados e como evitar isso

Dois comportamentos destroem a confiança nesses bots, e ambos têm uma correção no código acima.

Rever tudo de uma vez. Um bot que deixa quarenta comentários não terá nenhum deles lido. O limite de severidade e o máximo de dez comentários não são questões de cortesia. São o que mantém as descobertas relevantes visíveis. Ordenar por severidade antes de truncar faz com que o limite descarte as descobertas menos importantes, em vez de escolher dez ao acaso.

Comentar com confiança sobre algo que não consegue verificar. Este é o comportamento que faz os engenheiros desativarem o bot de vez. Um modelo que recebe 200 linhas de uma base de código com 40,000 linhas ainda escreverá "isto quebra a invalidação da cache em redis_client.py" sobre um ficheiro que nunca viu. O prompt do sistema é explícito: reporte apenas defeitos visíveis nas linhas apresentadas e omita tudo sobre o que não tiver certeza. Nomear diretamente a falha funciona melhor do que pedir precisão de forma genérica. Informar o modelo de que um resultado vazio é normal evita que invente algo para dizer sobre uma alteração de duas linhas. Os defeitos que só aparecem em todo o repositório são um trabalho diferente, e alojar o open-kritt localmente para análise de segurança é uma forma de abranger esse contexto sem ampliar o que este revisor pode ver.

Publique a revisão como COMMENT, nunca como REQUEST_CHANGES. A opinião de um modelo não deve poder bloquear um merge. Quando isso se torna possível, alguém com um prazo apertado removerá todo o workflow em vez de discutir com o bot. Quando quiser que o julgamento de um agente tenha peso real, faça-o produzir algo que possa ser verificado, em vez de algo em que tenha de acreditar. Essa é a ideia por trás de pedir a um agente um relatório de evidências que possa executar novamente por conta própria.

Modos de falha e as mensagens que verá

HTTP 422 ao publicar a revisão. gh apresenta gh: Unprocessable Entity (HTTP 422) e o corpo da resposta identifica o campo: Pull request review thread line must be part of the diff. O GitHub não consegue fixar esse comentário numa linha do diff. As causas habituais são um número de linha inventado pelo modelo, um path que ainda contém o prefixo b/ ou um comentário numa linha removida. Nesse último caso, é necessário definir side como LEFT em vez de RIGHT. Apresente o JSON da revisão antes de o publicar e confirme manualmente um comentário no diff.

invalid x-api-key da API do modelo. O passo falha na primeira chamada messages.create. O segredo ANTHROPIC_API_KEY não está definido no repositório ou o pull request veio de um fork, pelo que o Actions não recebeu segredos. A proteção contra forks na linha if: deveria ter ignorado o passo. Verifique primeiro essa linha.

gh: Resource not accessible by integration (HTTP 403). O token do job não pode escrever em pull requests. Adicione pull-requests: write ao bloco permissions:. Se já estiver presente, abra Settings, depois Actions e General. Uma política da organização pode limitar o que qualquer token de workflow pode solicitar.

json.decoder.JSONDecodeError. O modelo não devolveu JSON analisável. A causa comum é uma resposta que atingiu o limite de tokens e terminou a meio do objeto. A linha de log apresenta stop_reason precisamente para este caso: um valor de max_tokens significa que deve aumentar max_tokens ou reduzir MAX_COMMENTS.

O workflow nunca é executado. gh run list não apresenta nada para o pull request. Confirme que paths-ignore não filtrou todos os ficheiros alterados. Depois, verifique a proteção contra forks e a proteção por label. Confirme também se o runner está ativo com sudo systemctl status 'actions.runner.*' no VPS. Um runner offline deixa o job em fila sem qualquer mensagem de erro no pull request.

Todas as revisões ficam vazias. Defina MIN_SEVERITY como low numa execução. Se surgirem resultados, o limite está a funcionar. Se nada aparecer, apresente payload e confirme que os filtros não removeram todo o diff.

Executar em conjunto com os outros agentes

O reviewer é pequeno, por isso pode ser tentador instalá-lo no servidor que já executa tudo o resto. Mantenha-o separado se o repositório for importante. Este processo tem um token que pode comentar o seu código e uma chave que pode gastar o seu dinheiro. Um self-hosted runner é, por definição, um local onde o código dos workflows é executado. Uma conta dedicada sem privilégios e sem permissões de sudo, num host que não execute mais nada, é o mínimo recomendado. Se também executar agentes interativos que fazem checkout do código, uma VM descartável por agente é o padrão mais robusto. Executar um coding agent numa VPS explica a configuração geral. Se a Anthropic API for nova para si, uma primeira aplicação da Claude API numa VPS é um ponto de partida mais simples do que este.

FAQ

Um agente de revisão de PR baseado em IA precisa de acesso de escrita ao meu repositório?

Não. Precisa de pull-requests: write para publicar uma revisão e de contents: read para obter o diff. É tudo o que é necessário, e essas permissões são definidas no bloco permissions: do workflow, que limita o que o GITHUB_TOKEN de cada job pode fazer. Com essas duas linhas, o agente pode comentar um pull request, mas não pode enviar um commit nem fazer merge de um branch. Publique revisões com event: COMMENT em vez de REQUEST_CHANGES para que o agente também não possa bloquear um merge.

Porque é que o meu comentário de revisão falha com HTTP 422?

O GitHub aceita um comentário de revisão inline apenas numa linha que faça parte do diff do pull request e devolve Pull request review thread line must be part of the diff quando isso não acontece. Confirme que path é relativo à raiz do repositório e não inclui o prefixo b/ do cabeçalho do diff, e que o número da linha aparece dentro de um hunk desse ficheiro. side tem de ser RIGHT para uma linha adicionada ou inalterada e LEFT para uma linha removida. Adicionar o número da linha do novo ficheiro como prefixo a todas as linhas do diff antes de o enviar para o modelo impede que o modelo invente números desde o início.

Posso executar isto num repositório público com pull requests de forks?

Não com este design. O GitHub não transmite secrets a um workflow acionado a partir de um fork, pelo que a chave do modelo fica indisponível e a execução falha. O GitHub também afirma que os self-hosted runners "should almost never be used for public repositories", porque qualquer pessoa pode abrir um pull request que faça o código ser executado na sua máquina. Num projeto público, restrinja o reviewer a branches enviados diretamente para o próprio repositório, que é o que o guard if: faz, ou mova a etapa de revisão para um runner alojado pelo GitHub e aceite que o diff sai da sua própria infraestrutura.

Que modelo devo usar para a revisão de pull requests?

Comece pelo Haiku 4.5. Ler um diff limitado e compará-lo com uma lista fixa de tipos de defeitos não é um problema de raciocínio difícil, e o modelo mais barato mantém a fatura mensal num valor que não gera discussão. Passe para o Sonnet 5 se verificar que não deteta erros reais na sua linguagem ou framework e meça esse resultado em vez de o presumir. O Opus 5 é, por uma margem significativa, o mais caro dos três por pull request, o que é mais fácil de justificar num branch de release do que em cada push para cada branch de funcionalidade.