Manual code review is the bottleneck every dev team knows: pull requests sit for hours, reviewers skim, obvious bugs slip through, and senior engineers spend their day as human linters. In this tutorial, you'll build an AI code reviewer that runs automatically on every pull request, posts its findings as review comments, and costs pennies per review using a budget model.
By the end you'll have a working GitHub Action that reviews any PR diff and comments inline — about 100 lines of code total.
What we're building
Developer opens PR
│
▼
GitHub Actions triggers
│
▼
Fetch the PR diff via git
│
▼
Send diff to an LLM with a review prompt
│
▼
Parse structured findings → post PR commentWe'll use Python, GitHub Actions, and any OpenAI-compatible chat API. The pattern works with GPT, Claude, DeepSeek, GLM, or open-weights models — that's the point of OpenAI-compatible endpoints.
Step 1: The review client
Create reviewer/llm.py:
import os
import httpx
API_BASE = os.environ["LLM_API_BASE"] # e.g. https://api.qubax.ai/v1
API_KEY = os.environ["LLM_API_KEY"]
MODEL = os.environ.get("LLM_MODEL", "deepseek-v4-pro")
SYSTEM = """You are a senior code reviewer. Review the following diff.
Report ONLY genuine issues: bugs, security problems, edge cases, performance traps.
Do NOT comment on style, naming, or formatting.
Respond in this exact format, one issue per block:
FILE: <path>
LINE: <approx line in the new file>
SEVERITY: high | medium | low
ISSUE: <one-sentence description>
SUGGESTION: <concrete fix>
If there are no issues, respond with exactly: LGTM"""
def review_diff(diff: str) -> str:
# Keep the payload bounded: cap very large diffs.
max_chars = 60_000
if len(diff) > max_chars:
diff = diff[:max_chars] + "\n... (diff cut off for length)"
resp = httpx.post(
f"{API_BASE}/chat/completions",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"model": MODEL,
"messages": [
{"role": "system", "content": SYSTEM},
{"role": "user", "content": f"Review this diff:\n\ndiff\n{diff}\n```"}, ], "temperature": 0.1, "maxtokens": 2000, }, timeout=120, ) resp.raisefor_status() return resp.json()["choices"][0]["message"]["content"]
Note `temperature: 0.1` — for code review you want deterministic, focused output, not creativity.
## Step 2: Parse findings into structured issues
LLM output is only useful if you can act on it. Parse the format we demanded:
python
reviewer/parse.py
import re
BLOCK = re.compile( r"FILE:\s(?P<file>.+)\n" r"LINE:\s(?P<line>\d+)\n" r"SEVERITY:\s(?P<sev>high|medium|low)\n" r"ISSUE:\s(?P<issue>.+)\n" r"SUGGESTION:\s*(?P<suggestion>.+)", re.MULTILINE, )
def parse(text: str) -> list[dict]: if text.strip() == "LGTM": return [] out = [] for m in BLOCK.finditer(text): d = m.groupdict() d["line"] = int(d["line"]) out.append(d) return out
**Pro tip:** for production, prefer true structured output — pass `"response_format": {"type": "json_object"}` if your provider supports it, and ask for a JSON array. The regex approach above works with every OpenAI-compatible API, which is why we show it here.
## Step 3: The GitHub Action
Create `.github/workflows/ai-review.yml`:
yaml name: AI Code Review on: pull_request: types: [opened, synchronize]
permissions: pull-requests: write contents: read
jobs: review: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0
- uses: actions/setup-python@v5 with: python-version: "3.12"
- run: pip install httpx
- name: Fetch diff run: | git diff origin/${{ github.base_ref }}...HEAD > pr.diff wc -l pr.diff
- name: Run reviewer env: LLMAPIBASE: ${{ secrets.LLMAPIBASE }} LLMAPIKEY: ${{ secrets.LLMAPIKEY }} LLM_MODEL: deepseek-v4-pro run: python reviewer/main.py pr.diff
And `reviewer/main.py` to glue it together and post the comment:
python import os, sys, json, httpx from llm import review_diff from parse import parse
diff = open(sys.argv[1]).read() raw = review_diff(diff) issues = parse(raw)
if not issues: print("LGTM — no findings") sys.exit(0)
body = ["## 🤖 AI Code Review\n"] for i in issues: emoji = {"high": "🔴", "medium": "🟡", "low": "🔵"}[i["sev"]] body.append( f"### {emoji} {i['file']}:{i['line']} — {i['sev'].upper()}\n" f"Issue: {i['issue']}\n\n" f"Suggestion: {i['suggestion']}\n" )
prurl = os.environ.get("GITHUBAPIURL", "https://api.github.com") repo = os.environ["GITHUBREPOSITORY"] prnum = json.load(open(os.environ["GITHUBEVENTPATH"]))["pullrequest"]["number"]
httpx.post( f"{prurl}/repos/{repo}/issues/{prnum}/comments", headers={"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"}, json={"body": "\n".join(body)}, timeout=60, ) print(f"Posted {len(issues)} findings") ```
Add your LLM credentials under Settings → Secrets and variables → Actions (LLM_API_BASE, LLM_API_KEY), and every PR gets reviewed automatically.
Step 4: What it costs
This is where budget models change the math. A typical PR diff is 2,000–8,000 tokens. At budget-model pricing on Qubax — DeepSeek V4 Pro at roughly $0.08 per million input / $0.16 per million output tokens — a review of a 5,000-token diff costs around $0.001. One thousand PR reviews per month: about a dollar. The same reviews against a flagship at $5/M output would run 20–40x more, with marginal quality gain for the "find obvious bugs" task.
For higher-stakes repos, run a two-tier setup: budget model on every PR, escalate findings to a flagship model (e.g. Claude Opus 5.5) only for high-severity diffs. That's the pattern from our confidence-based LLM router tutorial.
Hardening tips
- Cap long diffs: if the diff is huge, review per-file instead of one blob — quality stays high and you stay under context limits.
- Avoid duplicate comments: store posted findings keyed by
file + line + issue hashand skip repeats on force-push re-reviews. - Filter noise: drop
lowseverity findings unless the PR is small; signal beats volume. - Never leak secrets: the diff goes to your LLM provider — exclude files matching secrets patterns (
.env,*.pem) before sending. - Set a cost cap: pass
max_tokensand log usage from the response'susagefield so a runaway diff can't blow your budget.
FAQ
Which model should I use for code review?
Budget models handle "find the bug" tasks well. Start with DeepSeek V4 Pro or GLM 4.7 Flash; escalate to GPT-5.2 Codex or Claude Opus-class models only if you see missed issues.
Does the AI see my proprietary code?
Yes — the diff is sent to your inference provider. Use a provider you trust contractually, strip secret files first, or use an E2EE routing option if that matters for your threat model.
Can I make it block merges on high-severity findings?
Yes — exit non-zero in the workflow and add a branch protection rule requiring the check. But keep it advisory at first; false positives will frustrate the team.
Does this work with GitLab?
The pattern is identical — swap the GitHub comment API for a GitLab merge-request note and run the job in .gitlab-ci.yml.
Stop paying flagship prices for routine reviews → [qubax.ai/models](https://qubax.ai/models)
Related: [Build a confidence-based LLM router](https://qubax.ai/blog/2026-09-18-build-confidence-based-llm-router-typescript-tutorial) · [Budget-capped streaming LLM client](https://qubax.ai/blog/2026-09-16-build-budget-capped-streaming-llm-client-python-tutorial) · [API docs](https://qubax.ai/docs)