<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mehmet Adem Şengül</title>
    <description>The latest articles on DEV Community by Mehmet Adem Şengül (@kissoid).</description>
    <link>https://dev.to/kissoid</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4097027%2F4ccb2a25-f038-4f6e-adff-85a202ce0e75.png</url>
      <title>DEV Community: Mehmet Adem Şengül</title>
      <link>https://dev.to/kissoid</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kissoid"/>
    <language>en</language>
    <item>
      <title>We didn't build a code-review agent. Here's why.</title>
      <dc:creator>Mehmet Adem Şengül</dc:creator>
      <pubDate>Thu, 27 Aug 2026 09:13:35 +0000</pubDate>
      <link>https://dev.to/kissoid/we-didnt-build-a-code-review-agent-heres-why-4mn0</link>
      <guid>https://dev.to/kissoid/we-didnt-build-a-code-review-agent-heres-why-4mn0</guid>
      <description>&lt;h1&gt;
  
  
  We didn't build a code-review agent. Here's why.
&lt;/h1&gt;

&lt;p&gt;We just shipped a Code Review API for &lt;a href="https://consensusroom.com" rel="noopener noreferrer"&gt;Consensus Room&lt;/a&gt;, and the most interesting part of building it wasn't the AI — it was deciding what &lt;em&gt;not&lt;/em&gt; to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tempting wrong answer
&lt;/h2&gt;

&lt;p&gt;The obvious pitch is: "point our agent at your repo, it reviews everything, done." That's what a lot of AI code review tools do. We almost built it too.&lt;/p&gt;

&lt;p&gt;Then we looked at what it would actually require:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tool-calling/function-calling wired up per LLM provider (we had zero of this — our stack is pure text completion via Claude, GPT, Gemini, and a dozen OpenRouter models)&lt;/li&gt;
&lt;li&gt;A sandboxed execution environment to let models poke around an arbitrary user-submitted repo — a real security surface on a multi-tenant SaaS&lt;/li&gt;
&lt;li&gt;Open-ended cost. If an agent decides how many files to read and how many tool calls to make, you can't tell a user "this will cost $0.03" before you start. Our whole product's trust model is a pre-flight cost estimate before anything runs.&lt;/li&gt;
&lt;li&gt;Much higher latency — a multi-step tool loop per review vs. a couple of LLM calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is impossible. It's just a different, much bigger project than "let multiple models review this code and merge their findings."&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built instead
&lt;/h2&gt;

&lt;p&gt;A single, boring, fast endpoint: &lt;code&gt;POST /api/v1/code-review&lt;/code&gt;. You send one code unit — a file, a function, a diff — and get back a structured report from multiple models plus a merged verdict. That's it. No repo access, no tool calls, no crawling.&lt;/p&gt;

&lt;p&gt;The trick is who calls it. If &lt;em&gt;your own&lt;/em&gt; coding agent (Claude Code, Cursor, a CI script, whatever) already knows how to walk a repo and decide what's worth reviewing, it can just call our endpoint once per file as part of its own loop. We don't need to reinvent repo traversal — every coding agent already does that. We just need to be a really good "get a second, third, and fourth opinion on this specific piece of code" primitive that's cheap and fast to call repeatedly.&lt;/p&gt;

&lt;p&gt;This reframing cut the actual engineering scope by an order of magnitude, and it's honestly a better fit for how people already work with coding agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that was still a real problem: structured output across providers
&lt;/h2&gt;

&lt;p&gt;We wanted findings back as structured data — severity, category, line number — not just a paragraph of prose. Obvious answer: JSON mode.&lt;/p&gt;

&lt;p&gt;Except we support Claude, GPT, Gemini (via an OpenAI-compatible endpoint), and whatever OpenRouter has that week. Checking the actual Spring AI option classes: &lt;code&gt;OpenAiChatOptions&lt;/code&gt; has a &lt;code&gt;responseFormat&lt;/code&gt; field. &lt;code&gt;AnthropicChatOptions&lt;/code&gt; does not. There's no single JSON-mode contract that works uniformly across all of them.&lt;/p&gt;

&lt;p&gt;So we didn't fight it. Each reviewer is instructed to emit one strict line format instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- [SEVERITY:HIGH] [CATEGORY:security] [LINE:42] SQL injection — user input is concatenated directly into the query.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server regex-parses that into structured JSON for the API response. If a model ever drifts from the format, the raw text is still returned in full alongside the (possibly incomplete) parsed findings — a parsing miss never silently drops information, it just means one field is less structured than usual.&lt;/p&gt;

&lt;p&gt;The moderator step takes every reviewer's raw findings, deduplicates the ones multiple models independently flagged (&lt;code&gt;[CONFIRMED BY:2/3]&lt;/code&gt;), and closes with a single &lt;code&gt;VERDICT: APPROVE|COMMENT|REQUEST_CHANGES&lt;/code&gt; line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it resumable, because money is involved
&lt;/h2&gt;

&lt;p&gt;Every LLM call costs real money the moment it completes, and it's charged immediately — not batched at the end. That means a review that fails halfway through (a provider hiccup, balance running out mid-flight) must not throw away work you already paid for.&lt;/p&gt;

&lt;p&gt;So the run loop is checkpointed: every reviewer's finding is persisted the instant it comes back. If a review fails or pauses, calling it again reloads what's already stored and only retries what's missing — nothing gets recharged, nothing gets lost. This wasn't a nice-to-have we added later; it came directly from asking "what happens when this fails halfway, with a customer's money on the line."&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://consensusroom.com/api/v1/code-review?wait=90"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer cr_live_..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "code": "def get_user(username):\n    query = \"SELECT * FROM users WHERE username = \" + username\n    return db.execute(query)",
    "filename": "users.py",
    "panelists": [
      {"key": "claude", "model": "claude-haiku-4-5", "role": "security"},
      {"key": "gpt",    "model": "gpt-4o-mini",       "role": "bugs"}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full reference (fields, limits, error codes, the exact parsing regex) is in &lt;a href="https://consensusroom.com/documentation.ai.md" rel="noopener noreferrer"&gt;documentation.ai.md&lt;/a&gt; — written to be pasted straight into an agent's context.&lt;/p&gt;

&lt;p&gt;Would genuinely like feedback from anyone who's solved the "structured output across heterogeneous LLM providers" problem differently — curious if there's a cleaner approach we're missing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
