SSD Nodes Learn 🎉 VPS desde $4.99/mês
Guias Matt ConnorPor Matt Connor

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

Execute um revisor de PR em sua VPS com runner self-hosted, prompts limitados ao diff, filtros por caminho e tamanho, comentários inline e custo por PR.

O que um agente self-hosted de revisão de PR faz

Um agente self-hosted de revisão de PR é um programa pequeno executado num servidor que lhe pertence. Lê o diff de um pull request (PR) e envia apenas as linhas alteradas para um modelo. O resultado é publicado como comentários inline da revisão. Nunca faz checkout do seu branch e nunca lê um ficheiro que o pull request não tenha alterado. As únicas credenciais que guarda são uma chave de API (application programming interface) de um modelo e um token que pode publicar comentários e não pode fazer mais nada.

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

O que precisa 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 quando o registar, porque o workflow abaixo seleciona essa etiqueta.
  • Uma chave de API da Anthropic obtida na Claude Console.
  • Um repositório onde possa controlar quem pode abrir um pull request. Um repositório privado é o caso mais simples. A secção abaixo sobre forks aborda o caso público, mas a resposta é menos confortável.

Instalar o reviewer no 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 release a partir da 2.20 inclui a flag --input usada abaixo. Uma mensagem Command 'gh' not found indica que o componente universe não está ativado. Execute sudo add-apt-repository universe e tente novamente.

Onde a chave e o token ficam

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

ANTHROPIC_API_KEY é um segredo do repositório, definido em Settings, depois Secrets and variables e Actions. O GitHub cifra esse 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 outra forma. O Actions cria um token novo para cada job e destrói-o quando o job termina. As açõ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 um 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. Proteja a chave do modelo com o mesmo cuidado, porque ela gasta 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 sem ocultação. 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. Portanto, uma execução de pull_request iniciada a partir de um fork executa o seu script sem ANTHROPIC_API_KEY, e a primeira chamada à API falha com invalid x-api-key.

A correção tentadora é mudar o trigger 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. A própria documentação de segurança do GitHub afirma que esses workflows "são privilegiados, o que significa que partilham a mesma cache do branch principal com outros triggers 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".

A mesma documentação é clara sobre o runner: "Os self-hosted runners quase nunca devem ser usados em repositórios públicos no GitHub, porque qualquer utilizador pode abrir pull requests contra o repositório e comprometer o ambiente."

Isto determina duas opções de desenho. O job inclui uma proteção para só ser executado em branches enviados para o seu próprio repositório. Além disso, o workflow não tem qualquer passo actions/checkout. O agente nunca tem o branch no disco, pelo que um pull request malicioso é apenas texto enviado para um modelo. Ele não pode executar um script de build no seu VPS, porque nada no seu VPS o executa.

Obtenha o diff, não o repositório

Um pedido 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 por diff --git a/. Um gh: Not Found (HTTP 404) significa que o token não consegue ver o repositório, o que, num token pessoal de acesso granular, quase sempre indica que a permissão Pull requests ficou desativada.

Filtre antes de gastar um token

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

  • Filtros de caminho. Exclua ficheiros de bloqueio, diretórios de dependências incluídas, bundles minimizados e código gerado. Um comentário do modelo sobre package-lock.json é apenas ruído, e esses ficheiros representam frequentemente a maior parte dos bytes de um diff.
  • Um limite de tamanho. Acima desse limite, ignore a revisão e termine com estado de 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 as conclusões de gravidade alta e média, até ao máximo de dez, ordenadas da maior gravidade para a menor. Ninguém lê o comentário onze.

O script

Guarde este conteúdo como /opt/pr-review/review.py. O script lê a configuração a partir do ambiente, para que o workflow possa mudar de modelo sem alterações no 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

Separar o diff por ficheiro é o que permite filtrar por caminho. Numerar cada linha é o que faz os comentários da revisão apontarem para o local correto. O GitHub só aceita um comentário inline numa linha que faça parte do diff, por isso o modelo tem de indicar um número de linha real. Ao fornecer os números, permite-se que o modelo copie um deles 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 condição para 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. Caso contrário, esse marcador faria todos os números seguintes ficarem deslocados em uma unidade.

Ambas as saídas usam o status 0, não 1. Um pull request filtrado ou demasiado grande deve apresentar uma verificação verde. Uma verificação vermelha que não permite nenhuma ação humana é ignorada. Quando uma verificação é ignorada, todas as outras também acabam por 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,
)

Três detalhes desse bloco são essenciais. 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 consegue processar esse fence. O path remove um b/ inicial, porque esse prefixo vem do cabeçalho do diff e o GitHub espera 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 em vez de dez.

Quando não há nada a comunicar, o script não publica nada. Um bot que escreve "nenhum problema encontrado" em todos os pull requests ensina as pessoas a passar rapidamente por essa mensagem. Depois, elas também passam rapidamente pela revisão que era importante.

Integre-o no 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 todos os jobs. O grupo concurrency é importante para a faturação: sem ele, o envio de três correções rápidas para uma branch executa 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 parte ignora pull requests de forks, que falhariam de qualquer forma sem uma key. A segunda parte fornece à equipa um mecanismo para desativar a execução: adicione o label no-ai-review a um pull request e o job não será executado.

Abra um pull request e monitorize 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 sem problemas, esse é o resultado esperado.

Quanto custa uma revisão automatizada de pull request?

O diff representa quase toda a entrada, por isso o tamanho do diff define o preço. Abaixo está um diff medido de 500 linhas, juntamente com o system prompt. A contagem foi obtida através do 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 da versão 4.7 utilizam um tokenizer mais recente, que produz aproximadamente 30% mais tokens para a mesma entrada. A Anthropic documenta esta diferença na sua página de preços. Tenha-a 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 com o preço introdutório válido até 31 de agosto de 2026. Depois disso, 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"
  }
]

Isto corresponde a 1.4 cêntimos por pull request no Haiku 4.5 e a 9.1 cêntimos 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 de setembro de 2026, multiplique o valor do Sonnet 5 por 1.5.

Dois fatores podem aumentar a fatura real acima desta estimativa. O trigger synchronize executa uma revisão a cada push. Por isso, uma branch ativa com oito pushes custa oito revisões. A regra de concorrência só ajuda quando os pushes acontecem com pouca diferença de tempo. Os valores também pressupõem que os filtros de caminhos estão a funcionar. Um único lock file não filtrado pode duplicar a entrada.

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

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

Adicione esta linha depois de payload ser construído. Em seguida, execute manualmente o script contra 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. Por isso, não consome tokens de entrada nem de saída. Também utiliza o tokenizer correspondente ao modelo indicado. Execute-o sobre dez pull requests reais do seu próprio repositório e use a mediana em vez da média. Assim, uma migração muito grande não distorce a estimativa.

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

Dois comportamentos destroem a confiança nestes 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 os problemas reais visíveis. Ordenar por severidade antes de truncar faz com que o limite remova os problemas menos importantes, em vez de remover dez problemas aleatórios.

Comentar com confiança sobre algo que não consegue verificar. Este é o comportamento que leva os engenheiros a desativar o bot permanentemente. Um modelo ao qual são mostradas 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: comunicar apenas os defeitos visíveis nas linhas apresentadas e omitir tudo aquilo de que não tenha a certeza. Indicar diretamente o tipo de falha funciona melhor do que pedir precisão de forma genérica. Dizer ao modelo que um resultado vazio é normal impede-o de inventar algo para dizer sobre uma alteração de duas linhas.

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 é possível, alguém com um prazo apertado removerá todo o workflow, em vez de discutir com o bot.

Modos de falha e as mensagens apresentadas

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 associar esse comentário a uma linha. As causas mais comuns 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 compare manualmente um comentário com o 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 as permissões que qualquer token de workflow pode solicitar.

json.decoder.JSONDecodeError. O modelo não devolveu JSON válido para análise. A causa comum é uma resposta que atingiu o limite de tokens e terminou a meio do objeto. A linha do 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. Verifique se 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 apresentar uma mensagem de erro no pull request.

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

Executar em conjunto com os restantes agentes

O reviewer é pequeno, por isso pode parecer prático instalá-lo no servidor que já executa tudo o resto. Mantenha-o separado se o repositório for importante. Este processo mantém um token que pode comentar o seu código e uma key que pode gastar o seu dinheiro, e 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 de código, uma VM descartável por agente é o padrão mais seguro, e executar um coding agent num VPS apresenta a configuração geral. Se a API da Anthropic for nova para si, uma primeira aplicação da Claude API num 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. Essa é a lista completa. As permissões são definidas no bloco permissions: do workflow, que limita o que o GITHUB_TOKEN de cada job pode fazer. Com estas duas linhas, o agente pode comentar um pull request, mas não pode enviar um commit nem fazer merge de uma branch. Publique as 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. Caso contrário, devolve Pull request review thread line must be part of the diff. Confirme que path é relativo ao repositório e não inclui o prefixo b/ do cabeçalho do diff. Confirme também 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 cada linha do diff antes de o enviar para o modelo impede, desde o início, que o modelo invente números.

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

Não com este desenho. O GitHub não disponibiliza secrets a um workflow acionado a partir de um fork. Por isso, falta a chave do modelo 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 executar código na sua máquina. Num projeto público, restrinja o revisor a branches enviadas para o próprio repositório, que é o que a condição if: faz, ou mova a etapa de revisão para um GitHub-hosted runner e aceite que o diff sai da sua infraestrutura.

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

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