Add a Zero-API-Key LLM Review Gate to GitHub Actions with VibeThinker-3B
On June 17, 2026, nine researchers at Sina Weibo released VibeThinker-3B: a 3.1-billion-parameter reasoning model, MIT-licensed, post-trained on Alibaba's Qwen2.5-Coder-3B.

Introduction
On June 17, 2026, nine researchers at Sina Weibo released VibeThinker-3B: a 3.1-billion-parameter reasoning model, MIT-licensed, post-trained on Alibaba's Qwen2.5-Coder-3B. Its technical report claims 94.3 on AIME 2026 and 80.2 Pass@1 on LiveCodeBench v6, numbers that sit next to models two hundred times its size, and the AI community has spent the weeks since arguing about whether to believe them. Here is the more useful question: a model this small runs on a stock GitHub Actions runner, on CPU, with no API key, no secret, and no external service seeing your code. In this tutorial you wire VibeThinker-3B into a pull request workflow via llama.cpp, so every PR gets a first-pass review that costs runner minutes and nothing else.
What VibeThinker-3B is
VibeThinker-3B is a dense model, not a Mixture-of-Experts, which matters here because dense 3B quantizes cleanly and runs acceptably on CPU. WeiboAI post-trained it with a pipeline they call Spectrum-to-Signal: a diversity-driven optimization approach designed to elicit reasoning behavior that normally appears only in much larger models. The claims are self-reported and the skeptics have a word for the failure mode, benchmaxxing, and they have a point worth keeping: the same model scores 70.2 on GPQA-Diamond, so its general knowledge is unremarkable. It is a specialist in verifiable reasoning over math and code.
For a CI gate, that trade is exactly right. You are not asking it to know things. You are handing it one bounded diff and asking what broke.
Why run the model on the runner itself
Every hosted LLM review integration has the same three costs: an API key in your repo secrets, your private diffs leaving your infrastructure, and a per-token bill that scales with how active your team is. Running the model inside the job deletes all three. The weights are a ~2 GB file, the standard ubuntu-latest runner has 4 vCPUs and 16 GB of RAM, and llama.cpp exposes an OpenAI-compatible server on localhost. Nothing enters, nothing leaves, and the marginal cost of a review is the two extra minutes of runner time.
The catch is honest and worth stating up front: CPU inference on 4 vCPUs is slow, on the order of 10 to 20 tokens per second for a Q4-quantized 3B. You cap the diff size and the output length, and the gate stays inside a coffee break.
Get the quantized weights
Community GGUF quantizations of VibeThinker-3B are on Hugging Face. Q4_K_M is the sane default: about 2 GB on disk, minimal quality loss for this size class.
pip install huggingface_hub
hf download samuelchristlie/VibeThinker-3B-gguf \
VibeThinker-3B.Q4_K_M.gguf --local-dir ./modelsIn the workflow this download hides behind a cache step, so after the first run it costs seconds, not minutes.
Stand up llama.cpp inside the job
llama.cpp publishes prebuilt release binaries, so the job downloads, unpacks, and serves without a compile step.
curl -L -o llama.zip \
https://github.com/ggml-org/llama.cpp/releases/latest/download/llama-b6000-bin-ubuntu-x64.zip
unzip -q llama.zip -d llama
./llama/build/bin/llama-server \
--model ./models/VibeThinker-3B.Q4_K_M.gguf \
--ctx-size 8192 --threads 4 --port 8080 &
until curl -s http://localhost:8080/health | grep -q '"ok"'; do sleep 2; doneThe until loop is the part people forget. Model load takes a few seconds, and a review script that fires immediately gets connection refused and a red build unrelated to the code under review.
Extract a bounded diff
The model reviews the merge diff between the PR branch and its base. Bound it hard: an 8K context minus a system prompt and room to answer leaves roughly 5,000 tokens of diff budget.
git fetch origin ${BASE_REF} --depth=50
git diff origin/${BASE_REF}...HEAD -- . \
':(exclude)*.lock' ':(exclude)dist/*' > pr.diff
head -c 20000 pr.diff > pr_bounded.diffTruncating at 20,000 characters is crude and it is the correct crudeness: a 3B model reasoning over a sprawling diff produces worse findings than the same model reasoning over the first focused chunk. Large PRs get a partial review and a note saying so, which is also a nudge toward smaller PRs.
Ask for findings on localhost
The server speaks the Chat Completions format, so the review script is plain requests against localhost, no SDK required.
import json
import sys
import requests
GATE_PROMPT = """You are a strict code reviewer. Read the unified diff.
Report ONLY concrete defects: logic errors, unhandled failure paths,
security holes, off-by-one mistakes. Ignore style entirely.
Answer with JSON only: {"findings": [{"file": str, "issue": str,
"severity": "high"|"medium"|"low"}]}
Empty diff or no defects: {"findings": []}"""
def review(diff_text: str) -> list[dict]:
resp = requests.post(
"http://localhost:8080/v1/chat/completions",
json={
"model": "vibethinker-3b",
"temperature": 0,
"max_tokens": 900,
"messages": [
{"role": "system", "content": GATE_PROMPT},
{"role": "user", "content": diff_text},
],
},
timeout=600,
)
content = resp.json()["choices"][0]["message"]["content"]
start, end = content.find("{"), content.rfind("}")
if start == -1:
return []
try:
return json.loads(content[start:end + 1]).get("findings", [])
except json.JSONDecodeError:
return []Two defensive choices carry this function. max_tokens=900 stops a reasoning model from thinking out loud for ten minutes on CPU. The brace-slicing handles the chain-of-thought prefix these models sometimes emit before the JSON, and a parse failure degrades to an empty review instead of a failed pipeline.
Publish findings without failing the build
A 3B model's opinion should never block a merge. Write findings to the job summary and drop a PR comment with the built-in GITHUB_TOKEN, which is not an API key you manage; Actions injects it.
import os
def publish(findings: list[dict]):
if not findings:
print("VibeThinker-3B gate: nothing to flag.")
return
rows = "\n".join(
f"| {item['severity']} | `{item['file']}` | {item['issue']} |"
for item in findings
)
table = f"| severity | file | issue |\n|---|---|---|\n{rows}"
with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as summary:
summary.write(f"## VibeThinker-3B first pass\n\n{table}\n")
print(table)Pipe the same table through gh pr comment "$PR_NUMBER" --body-file in a final workflow step if you want it visible in the conversation view.
The full workflow
name: llm-review-gate
on: pull_request
jobs:
vibethinker-review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with: {fetch-depth: 50}
- uses: actions/cache@v4
with:
path: ./models
key: vibethinker-3b-q4km
- name: Download weights if uncached
run: |
if [ ! -f ./models/VibeThinker-3B.Q4_K_M.gguf ]; then
pip install huggingface_hub
hf download samuelchristlie/VibeThinker-3B-gguf \
VibeThinker-3B.Q4_K_M.gguf --local-dir ./models
fi
- name: Serve VibeThinker-3B
run: |
curl -L -o llama.zip https://github.com/ggml-org/llama.cpp/releases/latest/download/llama-b6000-bin
unzip -q llama.zip -d llama
./llama/build/bin/llama-server \
--model ./models/VibeThinker-3B.Q4_K_M.gguf \
--ctx-size 8192 --threads 4 --port 8080 &
until curl -s http://localhost:8080/health | grep -q '"ok"'; do sleep 2; done
- name: Review the diff
env:
BASE_REF: ${{ github.base_ref }}
run: |
git fetch origin ${BASE_REF} --depth=50
git diff origin/${BASE_REF}...HEAD -- . \
':(exclude)*.lock' ':(exclude)dist/*' > pr.diff
head -c 20000 pr.diff > pr_bounded.diff
pip install requests
python review_gate.py pr_bounded.diffWith review_gate.py holding the two Python functions above plus a three-line main that reads the diff file, calls review, and calls publish.
On-runner VibeThinker-3B vs a hosted review API
| Dimension | VibeThinker-3B on the runner | Hosted LLM review API |
|---|---|---|
| Secrets required | None | API key in repo secrets |
| Where the diff goes | Nowhere, localhost only | The vendor |
| Marginal cost per PR | ~2 to 4 runner minutes | Per-token, forever |
| Quality ceiling | 3B specialist, single-file reasoning | Frontier, cross-file context |
| Rate limits | None | The vendor's |
| License | MIT, weights are yours | Terms of service |
What this gate will miss
It reviews one bounded diff with no repository context, so a renamed function whose distant caller broke is invisible to it. Three billion parameters of reasoning is real but shallow: it catches the unhandled exception and the inverted conditional, not the subtle race. The benchmark claims it shipped with are self-reported and independent verification is still pending, which is precisely why it publishes findings instead of failing builds (a gate that blocks merges on an unverified model's judgment is how teams learn to hate both the gate and the model). And on a busy repo, two to four minutes of CPU inference per PR is a real line on your Actions bill. It is just a far smaller line than the alternatives.
When to use it
Use this on private repositories where sending diffs to an external API is a conversation with security you would rather not schedule, and on public projects that want review assistance without managing secrets for fork PRs. Treat the output as a pre-reviewer: the human still reads the code, they just read it with three flagged lines already highlighted. If the findings prove consistently useful, the upgrade path is swapping the GGUF file for a bigger open-weight model on a GPU runner, and the workflow does not change by a single step. That is the property worth designing for: the model is a file in this pipeline, and files are easy to replace.
You might also like
Keep reading from the journal.
July 6, 2026AI
Your growth chart is counting the same person twice
Count people, not devices, and let the graph rewrite yesterday
June 30, 2026AI
Your shoppers stopped typing keywords. Make search catch up.
When customers describe what they want and search hears nothing
June 29, 2026AI
Define the metric once, or argue about it forever
One definition, or an argument that never ends