Tensor LabsTENSORLABS

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.

July 8, 20267 min read12 sectionsBy Ahmed Abdullah
Add a Zero-API-Key LLM Review Gate to GitHub Actions with VibeThinker-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.

code
pip install huggingface_hub
hf download samuelchristlie/VibeThinker-3B-gguf \
VibeThinker-3B.Q4_K_M.gguf --local-dir ./models

In 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.

bash
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; done

The 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.

bash
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

Truncating 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.

python
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.

python
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

bash
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.diff

With 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

DimensionVibeThinker-3B on the runnerHosted LLM review API
Secrets requiredNoneAPI key in repo secrets
Where the diff goesNowhere, localhost onlyThe vendor
Marginal cost per PR~2 to 4 runner minutesPer-token, forever
Quality ceiling3B specialist, single-file reasoningFrontier, cross-file context
Rate limitsNoneThe vendor's
LicenseMIT, weights are yoursTerms 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.