22 October 2026 · 5 min read
If you let an AI write code into a real project, something should read that code before it lands. The obvious move is to ask the same model to check its own work. It rarely finds much, because a model reviewing its own output brings the same training, the same habits and the same blind spots to the second read.
A reviewer from a different model family is a much better second pair of eyes. This is how SPOE checks every change it makes, and it takes about sixty lines to do yourself on Venice, whose API is OpenAI-compatible and keeps no copy of prompts on its private models.
Make it impossible to configure the same family twice. SPOE refuses to start if the writer and the reviewer match.
const FAMILIES: [RegExp, string][] = [ [/qwen|qwq/i, 'qwen'], [/glm|zai/i, 'glm'], [/llama|hermes/i, 'llama'], [/deepseek/i, 'deepseek'], [/mistral|devstral|codestral/i, 'mistral'], [/kimi|moonshot/i, 'kimi'], [/claude/i, 'claude'], [/gpt|openai/i, 'openai'], [/gemini|gemma/i, 'google'], [/grok/i, 'grok'], ]; const familyOf = (model: string) => FAMILIES.find(([re]) => re.test(model))?.[1] ?? null; const WRITER = 'qwen3-coder-480b-a35b-instruct-turbo'; const REVIEWER = 'zai-org-glm-5'; if (!familyOf(WRITER) || familyOf(WRITER) === familyOf(REVIEWER)) { throw new Error('The reviewer must come from a different model family than the writer.'); }
Venice speaks the OpenAI chat completions format. include_venice_system_prompt: false keeps Venice's default system prompt out of the way, so yours is the only one.
include_venice_system_prompt: false
async function chat(model: string, system: string, user: string, json = false): Promise<string> { const res = await fetch('https://api.venice.ai/api/v1/chat/completions', { method: 'POST', headers: { authorization: `Bearer ${process.env.VENICE_API_KEY}`, 'content-type': 'application/json' }, body: JSON.stringify({ model, temperature: json ? 0 : 0.2, messages: [{ role: 'system', content: system }, { role: 'user', content: user }], ...(json ? { response_format: { type: 'json_object' } } : {}), venice_parameters: { include_venice_system_prompt: false }, }), }); if (!res.ok) throw new Error(`Venice answered ${res.status}`); return (await res.json()).choices[0].message.content; }
The writer gets the request. The reviewer gets the request and the change, never the writer's reasoning, and answers in a fixed JSON shape so your code can act on it.
const REVIEW_RULES = `You are a security reviewer. You did not write this code. Find flaws before a human approves it, most severe first: CRITICAL: injection, auth bypass, secrets in code, arbitrary file access, one user reaching another's data. HIGH: data readable with no sign-in, missing validation at boundaries, weak password storage, XSS, SSRF. MEDIUM: missing rate limits on sign-in, error details leaked, insecure defaults. LOW: things that could become bugs. Do not review style. Do not rewrite the code. Answer with one JSON object: { "verdict": "pass" | "warn" | "block", "findings": [ { "severity": "critical"|"high"|"medium"|"low", "path": "...", "issue": "...", "fix": "..." } ] }`; type Finding = { severity: string; path?: string; issue: string; fix?: string }; const request = 'Add an endpoint that returns a booking by its id.'; const change = await chat(WRITER, 'You write TypeScript for a Fastify and Postgres app. Reply with the full changed files.', request); const answer = await chat(REVIEWER, REVIEW_RULES, `The request:\n${request}\n\nThe change:\n${change}`, true);
The model's verdict is advice. These three rules are what turn it into a gate.
function decide(raw: string): { verdict: 'pass' | 'warn' | 'block' | 'error'; findings: Finding[] } { let parsed: { verdict?: string; findings?: Finding[] }; try { parsed = JSON.parse(raw); } catch { // Rule 1: a review that fails or cannot be read is never a pass. It waits for a human. return { verdict: 'error', findings: [{ severity: 'high', issue: 'The review could not be read. This change was not reviewed.' }] }; } const findings = Array.isArray(parsed.findings) ? parsed.findings : []; const said = String(parsed.verdict ?? '').toLowerCase(); if (!['pass', 'warn', 'block'].includes(said)) return { verdict: 'error', findings }; // Rule 2: do not trust the roll-up. A critical finding blocks, whatever verdict the model wrote. if (findings.some((f) => f.severity === 'critical')) return { verdict: 'block', findings }; // Rule 3: a "pass" with high or medium findings is a warning, so a person sees them. if (said === 'pass' && findings.some((f) => ['high', 'medium'].includes(f.severity))) return { verdict: 'warn', findings }; return { verdict: said as 'pass' | 'warn' | 'block', findings }; } let review = decide(answer); if (review.verdict === 'error') { // Models sometimes answer in another shape. Ask once more with the rule repeated; a second miss stays an error. const again = `${REVIEW_RULES}\n\nYour last answer could not be read. Answer with one JSON object whose "verdict" is exactly "pass", "warn" or "block".`; review = decide(await chat(REVIEWER, again, `The request:\n${request}\n\nThe change:\n${change}`, true)); } if (review.verdict === 'pass') { // apply the change } else { // show the diff and the findings to a person, who approves or sends it back }
In practice the second family catches the expensive mistakes: an endpoint that returns any booking to anyone who guesses an id, a query built by joining strings, a key pasted into a file. It will not catch everything, and it is not a substitute for tests or for reading what matters to you. It is a cheap, fast second read that does not share the writer's blind spots, and the rules above keep a confused or unreachable reviewer from waving a change through.
In SPOE every generated change goes through exactly this before it reaches the project: a writer and a reviewer from different families, a critical finding that blocks whatever the verdict says, and a review that fails waiting for you instead of passing. Changes that pass go in on their own as a version you can undo; the rest wait in the diff view. Every export then records which model wrote and which reviewed each version, signed, so anyone you hand the code to can check it at spoe.ai/verify.
If you would rather not wire it up yourself, start building on spoe.ai: sign in with Google or GitHub and the review is on from the first change.
How it works · Features · Models · Pricing · Docs · Blog · Handle checker · App ideas · Arcade · Compare builders · Verify an export · Developer API · Start free
This page needs JavaScript to run the app itself. Everything above is a plain description of it.