Build an Automated Pull Request Review Bot with Moonshot Kimi K2.7-Code
In June 2026 Moonshot AI open-sourced Kimi K2.7-Code under a Modified MIT license.

Introduction
In June 2026 Moonshot AI open-sourced Kimi K2.7-Code under a Modified MIT license. That license is the whole story for this tutorial: a frontier-grade coding model you can call through an OpenAI-compatible endpoint, with no per-seat review tool sitting between you and your own repository. By the end you will have a bot that reads a pull request diff, reasons about it with Kimi K2.7-Code, and posts line-aware review comments back to GitHub. Roughly 150 lines, runnable today.
A code review bot is a good first real use of an open coding model because the task is bounded. You are not asking the model to write the feature. You are asking it to read a diff and tell you what it would flag. That is exactly the shape these models are now good at.
What Kimi K2.7-Code is
Kimi K2.7-Code is Moonshot AI's open-weight coding model. Two things matter for a review bot. First, it speaks the OpenAI Chat Completions format, so the official openai Python client works against it once you point base_url at Moonshot's API. Second, the Modified MIT license means you can also self-host the weights later and swap the base URL without touching the rest of this code. Start with the hosted endpoint, move to self-hosted when volume justifies it.
What a review bot actually does
Strip the marketing and a review bot is four steps. Fetch the diff for a pull request. Send the diff to a model with instructions about what to flag. Parse the response into structured findings. Post each finding back to the right file and line. The model does one of those steps. The other three are plumbing, and the plumbing is where most bots quietly break.
Set up the project
You need two packages: the GitHub SDK and the OpenAI client. The OpenAI client is the transport layer for Kimi K2.7-Code.
python -m venv venv && source venv/bin/activate
pip install PyGithub openai pydantic
export GITHUB_TOKEN="ghp_your_token_with_repo_scope"
export MOONSHOT_API_KEY="sk-your-moonshot-key"The GITHUB_TOKEN needs repo scope to read diffs and write review comments. The MOONSHOT_API_KEY comes from your Moonshot AI console. Both stay in the environment, never in the code.
Fetch the pull request diff
The first build step pulls the changed files for one pull request. PyGithub exposes each file's patch, which is the unified diff hunk for that file. That patch is all the model needs.
import os
from github import Github
def fetch_pr_files(repo_name: str, pr_number: int):
gh = Github(os.environ["GITHUB_TOKEN"])
repo = gh.get_repo(repo_name)
pr = repo.get_pull(pr_number)
files = []
for f in pr.get_files():
if f.patch is None: # binary files have no patch
continue
files.append({
"filename": f.filename,
"patch": f.patch,
"status": f.status,
})
return pr, filesSkipping files where patch is None matters. Binary files, images, and lock files above GitHub's diff size limit return no patch, and feeding None to the model wastes tokens and produces hallucinated line numbers.
Call Kimi K2.7-Code with a review instruction
Now point the OpenAI client at Moonshot and ask Kimi K2.7-Code to review one file. The system prompt is the contract: it tells the model what to look for and, critically, what format to answer in.
from openai import OpenAI
client = OpenAI(
api_key=os.environ["MOONSHOT_API_KEY"],
base_url="https://api.moonshot.ai/v1",
)
REVIEW_SYSTEM = """You are a senior engineer reviewing one file's diff.
Flag only real problems: bugs, security issues, missing error handling,
broken edge cases. Do not comment on style or formatting.
For each issue return the line from the diff and a one-sentence reason.
If the diff is clean, return an empty list."""
def review_file(filename: str, patch: str) -> str:
response = client.chat.completions.create(
model="kimi-k2.7-code",
temperature=0,
messages=[
{"role": "system", "content": REVIEW_SYSTEM},
{"role": "user", "content": f"File: {filename}\n\nDiff:\n{patch}"},
],
)
return response.choices[0].message.contenttemperature=0 is deliberate. A review bot that returns different findings on the same diff every run is worse than no bot, because nobody trusts a reviewer who changes their mind for no reason.
Force the output into a schema
Free-text findings are unusable for posting comments, because you cannot map prose back to a file and line. Define the shape you want and ask the model to fill it. Pydantic both documents the schema and validates the response.
import json
from pydantic import BaseModel, ValidationError
class Finding(BaseModel):
line_hint: str # a quoted line from the diff
severity: str # "high" | "medium" | "low"
reason: str
class Review(BaseModel):
findings: list[Finding]
def review_file_structured(filename: str, patch: str) -> Review:
response = client.chat.completions.create(
model="kimi-k2.7-code",
temperature=0,
response_format={"type": "json_object"},
messages=[
{"role": "system", "content": REVIEW_SYSTEM +
'\nReturn JSON: {"findings": [{"line_hint": str,'
' "severity": str, "reason": str}]}'},
{"role": "user", "content": f"File: {filename}\n\nDiff:\n{patch}"},
],
)
raw = response.choices[0].message.content
try:
return Review(**json.loads(raw))
except (json.JSONDecodeError, ValidationError):
return Review(findings=[]) # never crash the bot on a bad responseThe except clause is not optional. An open model on an OpenAI-compatible endpoint will occasionally return JSON with a trailing comment or a wrapped code fence. A review bot that throws on one malformed response and abandons the other nine files is a bot nobody re-runs.
Post the findings back to GitHub
The last build step writes a single review summarising every finding. Posting one review beats posting twenty inline comments, because twenty bot comments on a PR trains the team to collapse the bot.
def post_review(pr, results: dict[str, Review]):
lines = []
for filename, review in results.items():
for f in review.findings:
lines.append(
f"- **{f.severity.upper()}** `{filename}` "
f"near `{f.line_hint.strip()[:80]}`: {f.reason}"
)
if not lines:
body = "Kimi K2.7-Code review: no issues found."
else:
body = "### Kimi K2.7-Code review\n\n" + "\n".join(lines)
pr.create_issue_comment(body)Kimi K2.7-Code vs a closed coder API
The choice between an open model and a closed coder API is not about quality at this point. It is about what you control. | Dimension | Kimi K2.7-Code (open) | Closed coder API | |-----------|------------------------|------------------| | License | Modified MIT, self-hostable | Vendor terms, hosted only | | Where the diff goes | Moonshot now, your hardware later | The vendor, always | | Cost model | API now, fixed infra at volume | Per-token, scales with PRs | | Version pinning | Pin the weights you downloaded | Vendor deprecates on their schedule | | Data residency | Yours once self-hosted | Vendor region |
For a team reviewing private code, the line "your hardware later" is the one that matters. You can prototype against the hosted endpoint this week and move the same code behind self-hosted weights when compliance asks where the diffs go.
What this bot does not do
It reviews each file in isolation, so it cannot catch a bug that spans two files, like a renamed function whose caller was not updated. It trusts line_hint as a string match rather than a true diff position, which is good enough for a summary comment but not for GitHub's line-anchored review API. And it has no memory between runs, so it will re-flag an issue the author intentionally left as is. None of these are reasons not to ship it. They are the next three tickets.
When to use it
Run this on internal pull requests where a fast, consistent second pass catches the obvious miss before a human spends attention on it. It is not a replacement for human review and it should never be a merge gate, because a model that confidently approves a test asserting the wrong thing fails differently than a tired human does. Use it as the reviewer that never gets bored of reading diffs, and keep the human for the judgment the bot cannot have. The open license means the day you outgrow the hosted endpoint, the only line that changes is the base URL.
Full working example
import os
import json
from github import Github
from openai import OpenAI
from pydantic import BaseModel, ValidationError
client = OpenAI(
api_key=os.environ["MOONSHOT_API_KEY"],
base_url="https://api.moonshot.ai/v1",
)
REVIEW_SYSTEM = """You are a senior engineer reviewing one file's diff.
Flag only real problems: bugs, security issues, missing error handling,
broken edge cases. Do not comment on style or formatting.
Return JSON: {"findings": [{"line_hint": str, "severity": str, "reason": str}]}
If the diff is clean, return {"findings": []}."""
class Finding(BaseModel):
line_hint: str
severity: str
reason: str
class Review(BaseModel):
findings: list[Finding]
def fetch_pr_files(repo_name: str, pr_number: int):
gh = Github(os.environ["GITHUB_TOKEN"])
pr = gh.get_repo(repo_name).get_pull(pr_number)
files = [
{"filename": f.filename, "patch": f.patch}
for f in pr.get_files() if f.patch is not None
]
return pr, files
def review_file(filename: str, patch: str) -> Review:
resp = client.chat.completions.create(
model="kimi-k2.7-code",
temperature=0,
response_format={"type": "json_object"},
messages=[
{"role": "system", "content": REVIEW_SYSTEM},
{"role": "user", "content": f"File: {filename}\n\nDiff:\n{patch}"},
],
)
try:
return Review(**json.loads(resp.choices[0].message.content))
except (json.JSONDecodeError, ValidationError):
return Review(findings=[])
def post_review(pr, results: dict[str, Review]):
lines = []
for filename, review in results.items():
for f in review.findings:
lines.append(
f"- **{f.severity.upper()}** `{filename}` "
f"near `{f.line_hint.strip()[:80]}`: {f.reason}"
)
body = ("### Kimi K2.7-Code review\n\n" + "\n".join(lines)) if lines \
else "Kimi K2.7-Code review: no issues found."
pr.create_issue_comment(body)
def main(repo_name: str, pr_number: int):
pr, files = fetch_pr_files(repo_name, pr_number)
results = {f["filename"]: review_file(f["filename"], f["patch"]) for f in files}
post_review(pr, results)
if __name__ == "__main__":
main("your-org/your-repo", 42)Wire main into a GitHub Actions workflow triggered on pull_request, pass the repo and PR number from the event payload, and every new pull request gets a Kimi K2.7-Code pass before a human opens it.
You might also like
Keep reading from the journal.
June 12, 2026AI
Giving your agents a browser without giving each one a Chrome
Smooth, a serverless browser agent API, shipped in the May-June 2026 open-source-adjacent tooling wave.
June 11, 2026AI
Your parser is not your product
The consultation call was about emails. A parts-sourcing platform for industrial components, the kind of business where a buyer sends a bill of materials as three paragraphs of prose and somebody on the other end retypes it into a quote system before lunch
June 10, 2026AI
Running a frontier coder on hardware you rent
On June 1, 2026, MiniMax released MiniMax M3, the first open-weight model to pair frontier coding with a 1M-token context and native multimodality. It tops the open-weight SWE-Bench Pro leaderboard at 59.0%.