Build a code review assistant
Pipe a git diff into the Bike4Mind completions API and get back structured findings, each with a severity, the file, and a suggested fix, that a script or CI job can act on.
- Verified
- Every example run against the live API on
- Endpoints
GET /api/v1/models,POST /api/ai/v1/completions- Key scopes
ai:chat- Model
claude-haiku-4-5-20251001- Written in
- curl, TypeScript
A reviewer that reads a diff, holds it against your team's standards, and returns findings as data rather than prose. Because the output is JSON that matches a schema you wrote, a script can sort it, post it as PR comments, or fail a build on a blocker.
You need curl, jq, and Node 22 or later. The TypeScript step uses npx tsx.
Create an API key
In the Bike4Mind app, open your profile, go to the API Keys tab, and click Create API Key. Give it the AI Chat scope (or AI Generate; the completions endpoint accepts either). Copy the key once and keep it in your shell, not in your code:
export B4M_API_KEY="b4m_live_<your key>"
Check that the key works by listing the text models it can use:
curl -s "https://app.bike4mind.com/api/v1/models?limit=100" \
-H "Authorization: Bearer $B4M_API_KEY" \
| jq -r '.data[] | select(.type == "text") | .id'
anthropic.claude-fable-5
anthropic.claude-fable-5-1
...
claude-haiku-4-5-20251001
...
grok-4.20-0309-reasoning
That is the first page of results. When the response's next_cursor is not null, pass it back as
cursor for the next page.
Every example below uses claude-haiku-4-5-20251001, a small model that is cheap to run. Any text
model from that list works.
Ask for a review with a JSON schema
POST /api/ai/v1/completions takes OpenAI-style messages. Add a response_format of type
json_schema and the reply is a JSON document that matches the schema. The system message carries
the team's standards; the user message carries the diff.
curl -sN -X POST "https://app.bike4mind.com/api/ai/v1/completions" \
-H "Authorization: Bearer $B4M_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'JSON'
{
"model": "claude-haiku-4-5-20251001",
"messages": [
{
"role": "system",
"content": "You review code diffs. Report only real problems: bugs, security issues, and violations of the team's standards. Standards: no unparameterized SQL, no secrets in code, handle errors explicitly."
},
{
"role": "user",
"content": "diff --git a/api/users.js b/api/users.js\n+app.get('/users', async (req, res) => {\n+ const rows = await db.query(`SELECT * FROM users WHERE name = '${req.query.name}'`);\n+ res.json(rows);\n+});"
}
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "code_review",
"schema": {
"type": "object",
"properties": {
"summary": { "type": "string" },
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"file": { "type": "string" },
"severity": { "type": "string", "enum": ["blocker", "major", "minor"] },
"issue": { "type": "string" },
"suggestion": { "type": "string" }
},
"required": ["file", "severity", "issue", "suggestion"]
}
}
},
"required": ["summary", "findings"]
}
}
}
}
JSON
Read the stream
The endpoint always answers with Server-Sent Events, whatever you set stream to. The first event is
meta, the JSON arrives in pieces across content events, and the last content event carries token
usage and the credits the call cost. The stream ends with data: [DONE]. This is the response from
the request above, with the middle cut:
data: {"type":"meta","requestId":"21140188-35f3-4450-a4ef-d152482d40a1"}
data: {"type":"content","text":"{\"summa","responseFormatMode":"tool_use"}
data: {"type":"content","text":"ry\": \"C","responseFormatMode":"tool_use"}
...
data: {"type":"content","text":"","usage":{"inputTokens":915,"outputTokens":277},"credits":{"used":5,"usdCost":0.0023},"responseFormatMode":"tool_use","stopReason":"tool_use"}
data: [DONE]
Joining the text of every content event gives the review:
{
"summary": "Critical SQL injection vulnerability detected in user endpoint. Query parameter is directly interpolated into SQL string without parameterization.",
"findings": [
{
"file": "api/users.js",
"severity": "blocker",
"issue": "SQL Injection vulnerability: User-supplied query parameter 'name' is directly interpolated into SQL query using string template literal without parameterization",
"suggestion": "Use parameterized queries or prepared statements. Replace with: `await db.query('SELECT * FROM users WHERE name = ?', [req.query.name])`"
},
{
"file": "api/users.js",
"severity": "major",
"issue": "Missing error handling: No try-catch block or error handling for database query that could fail",
"suggestion": "Wrap the database call in a try-catch block and return appropriate error responses to the client (e.g., return res.status(500).json({error: 'Database error'}))"
}
]
}
Two details worth knowing. With an Anthropic model, the schema is enforced through a tool call, so
responseFormatMode is tool_use and stopReason is tool_use rather than end_turn. And once the
stream has opened the HTTP status stays 200: an error, including running out of credits, arrives as
an event with "type":"error" and a code such as insufficient_credits. Check for it.
Turn it into a script
review.mts reads a diff on stdin, sends it with the same schema, parses the stream, prints the
findings, and exits with status 1 if any finding is a blocker. That exit code is what lets a CI job
or a pre-push hook stop on it.
// review.mts: pipe a diff in, get structured review findings out.
// Usage: git diff origin/main | npx tsx review.mts
const API = "https://app.bike4mind.com/api/ai/v1/completions";
const MODEL = "claude-haiku-4-5-20251001";
const STANDARDS = [
"No unparameterized SQL.",
"No secrets or credentials in code.",
"Handle errors explicitly; never swallow them.",
];
type Finding = { file: string; severity: "blocker" | "major" | "minor"; issue: string; suggestion: string };
type Review = { summary: string; findings: Finding[] };
async function readStdin(): Promise<string> {
let input = "";
for await (const chunk of process.stdin) input += chunk;
return input;
}
async function review(diff: string): Promise<Review> {
const res = await fetch(API, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.B4M_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: MODEL,
messages: [
{
role: "system",
content: `You review code diffs. Report only real problems. Team standards:\n${STANDARDS.join("\n")}`,
},
{ role: "user", content: diff },
],
response_format: {
type: "json_schema",
json_schema: {
name: "code_review",
schema: {
type: "object",
properties: {
summary: { type: "string" },
findings: {
type: "array",
items: {
type: "object",
properties: {
file: { type: "string" },
severity: { type: "string", enum: ["blocker", "major", "minor"] },
issue: { type: "string" },
suggestion: { type: "string" },
},
required: ["file", "severity", "issue", "suggestion"],
},
},
},
required: ["summary", "findings"],
},
},
},
}),
});
if (res.ok === false || res.body === null) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
// The endpoint always streams Server-Sent Events. Concatenate the `text` of
// every content event; the result is the JSON document the schema describes.
let text = "";
let buffer = "";
const decoder = new TextDecoder();
for await (const chunk of res.body) {
buffer += decoder.decode(chunk, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (line.startsWith("data: {") === false) continue; // skips blanks, keep-alives and [DONE]
const event = JSON.parse(line.slice(6));
if (event.type === "error") throw new Error(`${event.code ?? "error"}: ${event.message}`);
if (event.type === "content") text += event.text;
}
}
return JSON.parse(text) as Review;
}
const diff = await readStdin();
if (diff.trim() === "") {
console.error("No diff on stdin.");
process.exit(2);
}
const result = await review(diff);
console.log(result.summary);
for (const f of result.findings) console.log(`- [${f.severity}] ${f.file}: ${f.issue}\n fix: ${f.suggestion}`);
process.exit(result.findings.some((f) => f.severity === "blocker") ? 1 : 0);
Run it against the changes on your branch:
git diff origin/main | npx tsx review.mts
Here it is on a diff that adds a hard-coded key and an empty catch:
diff --git a/src/config.ts b/src/config.ts
index 3b18e51..a1c2d3f 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,3 +1,8 @@
export const region = "us-east-1";
+export const stripeKey = "sk_live_example_not_a_real_key";
+
+export async function loadSettings(path: string) {
+ try { return JSON.parse(await readFile(path, "utf8")); } catch {}
+}
Multiple critical issues found in config.ts: hardcoded credential, missing import, and unhandled error.
- [blocker] src/config.ts: Hardcoded secret credential in code. Stripe API key should never be committed to repository, even as example.
fix: Remove the hardcoded stripeKey and load it from environment variables (e.g., process.env.STRIPE_KEY) or a secure secrets manager. Never commit actual or example API keys to version control.
- [blocker] src/config.ts: Silent error handling: catch block is empty, swallowing all errors. This violates the requirement to handle errors explicitly.
fix: Replace 'catch {}' with proper error handling: 'catch (error) { throw new Error(`Failed to load settings from ${path}: ${error}`); }' or log and handle appropriately.
- [major] src/config.ts: Missing import: 'readFile' function is used but not imported from 'fs/promises' or 'fs'.
fix: Add import statement: 'import { readFile } from "fs/promises";' at the top of the file.
The exit status was 1, because of the blockers. All three findings are real. The severities are the
model's judgment: a second run on the same diff rated the empty catch as major rather than
blocker. If the exit code gates a build, decide which severities should stop it and expect some
run-to-run variation.
Where to take it
- Your standards, not ours. The
STANDARDSarray is the whole policy. Put your team's real rules there, or read them from a file in the repo so they are reviewed like code. - Large diffs. The model sees only what you send. For a big change, review one file at a time
(
git diff origin/main -- path/to/file) and merge the findings. - It is a second reader, not a gate you trust blindly. Models miss things and flag things that are fine. Treat a blocker as a reason for a person to look, not as a verdict.
- Cost. The run above used 915 input and 277 output tokens, which the stream reported as 5 credits. Bigger diffs cost proportionally more; the last content event tells you exactly.
Something here no longer matches what the API returns? Tell us at support@bike4mind.com and we will rerun it. Full endpoint reference: API explorer.