Skip to content

feat: multi-model consensus review with parallel execution and finding dedup - #7

Merged
brucearctor merged 2 commits into
mainfrom
feat/multi-model-consensus
Jul 9, 2026
Merged

feat: multi-model consensus review with parallel execution and finding dedup#7
brucearctor merged 2 commits into
mainfrom
feat/multi-model-consensus

Conversation

@brucearctor

@brucearctor brucearctor commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Run 2+ AI models in parallel and only keep findings that multiple models agree on, drastically reducing false positives.

Usage

# Run Gemini Flash + Claude in consensus (both must agree)
code-reviewer --diff --models gemini-2.5-flash,claude-sonnet-4

# 3 models, require 2 to agree
code-reviewer --diff --models gemini-2.5-flash,gemini-2.5-pro,claude-sonnet-4 --consensus-threshold 2

# Via env var
export REVIEW_MODELS=gemini-2.5-flash,claude-sonnet-4
code-reviewer --diff

How it works

  1. Parallel execution — All models run concurrently via errgroup
  2. Finding deduplication — Two findings are "the same" if: same file, within ±3 lines, same category
  3. Consensus threshold — Only findings that >= N models agree on are kept (default: 2)
  4. Canonical selection — When models agree, the finding with the longest body is used
  5. Summary merge — Unique summaries from all models are concatenated

Files changed

File Change
internal/model/multi.go [NEW] MultiProvider, mergeResults, findingsMatch
internal/model/multi_test.go [NEW] 8 tests for dedup, threshold, edge cases
internal/config/config.go Models, ConsensusThreshold fields + flag/env support
cmd/code-reviewer/main.go Auto-switch to MultiProvider when len(models) > 1

Tests

112 total, all passing with -race. 8 new tests for multi-model consensus.

Summary by CodeRabbit

  • New Features
    • Added multi-model reviewing that runs providers in parallel and requires consensus before accepting merged results.
    • Introduced environment and command-line options to select multiple models and configure the consensus threshold.
  • Bug Fixes
    • Improved result aggregation by grouping near-matching findings, selecting a canonical finding, and merging unique summaries more consistently.
  • Tests
    • Added unit tests covering consensus/merging behavior, concurrency and error handling, provider closing, and robust parsing of review JSON across common formats.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4934a137-d066-4b0b-9a7b-d2c23719f66b

📥 Commits

Reviewing files that changed from the base of the PR and between 97cf14f and bcac196.

⛔ Files ignored due to path filters (2)
  • go.mod is excluded by !go.mod
  • go.sum is excluded by !**/*.sum, !go.sum
📒 Files selected for processing (4)
  • internal/config/config.go
  • internal/model/multi.go
  • internal/model/multi_test.go
  • internal/model/parse_test.go
🚧 Files skipped from review as they are similar to previous changes (4)
  • internal/model/parse_test.go
  • internal/config/config.go
  • internal/model/multi.go
  • internal/model/multi_test.go

📝 Walkthrough

Walkthrough

Configuration now accepts multiple models and a consensus threshold. The model layer adds a concurrent multi-provider that merges reviewer outputs by thresholded agreement, and tests cover matching, aggregation, orchestration, and JSON parsing behavior.

Changes

Multi-model consensus review

Layer / File(s) Summary
Config fields and loading
internal/config/config.go
Config adds Models []string and ConsensusThreshold int; env and flag loading now parse REVIEW_MODELS, --models, and --consensus-threshold with shared comma-splitting logic.
MultiProvider construction and lifecycle
internal/model/multi.go
ReviewProvider and MultiProvider are introduced, with constructors for model-backed and injected reviewers plus Close() for cleanup.
Concurrent review and consensus merge
internal/model/multi.go
Review() runs providers concurrently, mergeResults applies thresholded grouping and summary deduplication, and findingsMatch defines the line-proximity match rule.
Consensus and lifecycle tests
internal/model/multi_test.go
Tests cover finding proximity, threshold filtering, canonical selection, nil and single-model cases, summary merging, drift prevention, concurrent review behavior, partial errors, and close handling.
JSON parsing coverage
internal/model/parse_test.go
parseReviewJSON tests cover direct JSON, fenced JSON, surrounding text, findings arrays, malformed input, empty input, whitespace-wrapped input, and nested braces in text.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: multi-model consensus review with parallel execution and deduplication.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/multi-model-consensus

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
internal/model/multi.go (1)

15-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider a Provider interface to make MultiProvider (and its concurrency) unit-testable.

MultiProvider holds []*Provider (a concrete struct backed by the genai client). This means MultiProvider.Review's actual concurrent path can't be exercised with fakes in tests — multi_test.go only tests the pure helper functions (findingsMatch, mergeResults), leaving the concurrent execution and error-aggregation behavior of Review untested. Extracting a small Reviewer-style interface (Review(ctx, systemPrompt, userPrompt) (*ReviewResult, error)) would let tests inject fakes/mocks for MultiProvider.

As per path instructions for internal/**: "Focus on error handling, context propagation, and interface design."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/model/multi.go` around lines 15 - 51, MultiProvider is hard to
unit-test because it stores concrete *Provider values, which prevents injecting
fakes into its concurrent Review path. Introduce a small Reviewer-style
interface with the Review method, update MultiProvider to hold a slice of that
interface instead of []*Provider, and have NewMultiProvider build the concrete
providers behind that abstraction. Keep the existing concurrency and
error-handling behavior in MultiProvider.Review, but make it depend on the
interface so tests can cover concurrent execution and aggregation with mock
reviewers.

Source: Path instructions

internal/config/config.go (1)

294-303: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate comma-split/trim/filter logic between loadEnv and loadFlags.

The exact same "split on ,, trim, drop empties" loop is repeated verbatim for REVIEW_MODELS and --models. Extract a small helper to avoid drift if the parsing logic ever needs to change (e.g. dedup, case-normalization).

♻️ Proposed refactor
+// splitAndTrim splits s on sep, trims whitespace from each part, and drops empty entries.
+func splitAndTrim(s, sep string) []string {
+	var result []string
+	for _, part := range strings.Split(s, sep) {
+		part = strings.TrimSpace(part)
+		if part != "" {
+			result = append(result, part)
+		}
+	}
+	return result
+}
 	if v := os.Getenv("REVIEW_MODELS"); v != "" {
-		var models []string
-		for _, m := range strings.Split(v, ",") {
-			m = strings.TrimSpace(m)
-			if m != "" {
-				models = append(models, m)
-			}
-		}
-		c.Models = models
+		c.Models = splitAndTrim(v, ",")
 	}
 	if *models != "" {
-		var parsed []string
-		for _, m := range strings.Split(*models, ",") {
-			m = strings.TrimSpace(m)
-			if m != "" {
-				parsed = append(parsed, m)
-			}
-		}
-		c.Models = parsed
+		c.Models = splitAndTrim(*models, ",")
 	}

Also applies to: 376-388

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/config/config.go` around lines 294 - 303, The
comma-split/trim/filter parsing for REVIEW_MODELS is duplicated in loadEnv and
loadFlags, so extract it into a shared helper and use that from both paths.
Factor the repeated loop in config.go around loadEnv and loadFlags into a single
small parser function (for example, one that handles strings.Split,
strings.TrimSpace, and dropping empties) and assign c.Models from that helper so
future parsing changes stay consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/model/multi.go`:
- Around line 91-164: The grouping logic in mergeResults is using g.canonical as
both the match anchor and the display choice, which lets the canonical finding
shift and incorrectly merge distant findings through a chain. Update
mergeResults so each findingGroup keeps a fixed anchor for findingsMatch
comparisons, and use the longest-body Finding only as the canonical/display
result. Ensure findingsMatch continues to compare against the stable anchor
rather than the mutable canonical field.

---

Nitpick comments:
In `@internal/config/config.go`:
- Around line 294-303: The comma-split/trim/filter parsing for REVIEW_MODELS is
duplicated in loadEnv and loadFlags, so extract it into a shared helper and use
that from both paths. Factor the repeated loop in config.go around loadEnv and
loadFlags into a single small parser function (for example, one that handles
strings.Split, strings.TrimSpace, and dropping empties) and assign c.Models from
that helper so future parsing changes stay consistent.

In `@internal/model/multi.go`:
- Around line 15-51: MultiProvider is hard to unit-test because it stores
concrete *Provider values, which prevents injecting fakes into its concurrent
Review path. Introduce a small Reviewer-style interface with the Review method,
update MultiProvider to hold a slice of that interface instead of []*Provider,
and have NewMultiProvider build the concrete providers behind that abstraction.
Keep the existing concurrency and error-handling behavior in
MultiProvider.Review, but make it depend on the interface so tests can cover
concurrent execution and aggregation with mock reviewers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 38498832-851c-404e-a965-28bfc96790cc

📥 Commits

Reviewing files that changed from the base of the PR and between 8d5d4e7 and 88eb6bb.

⛔ Files ignored due to path filters (2)
  • go.mod is excluded by !go.mod
  • go.sum is excluded by !**/*.sum, !go.sum
📒 Files selected for processing (3)
  • internal/config/config.go
  • internal/model/multi.go
  • internal/model/multi_test.go

Comment thread internal/model/multi.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/model/multi_test.go (1)

205-217: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Weak assertion doesn't verify expected deterministic behavior.

m1.callCount.Load() == 0 && m2.callCount.Load() == 0 only fails if neither provider is ever called, which given MultiProvider.Review's use of errgroup.Go (each goroutine calls Review unconditionally before any context check) should never happen — both should deterministically be called exactly once regardless of m2's error. This makes the check nearly tautological and doesn't guard against a future correctness regression.

♻️ Tighten the assertion
-	if m1.callCount.Load() == 0 && m2.callCount.Load() == 0 {
-		t.Error("expected at least one model to be called")
-	}
+	if m1.callCount.Load() != 1 {
+		t.Errorf("m1 called %d times, want 1", m1.callCount.Load())
+	}
+	if m2.callCount.Load() != 1 {
+		t.Errorf("m2 called %d times, want 1", m2.callCount.Load())
+	}

As per path instructions, **/*_test.go files should be verified for proper race condition safety and meaningful assertions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/model/multi_test.go` around lines 205 - 217, The assertion in
TestMultiProviderReview_PartialError is too weak and does not verify the
deterministic behavior of MultiProvider.Review. Update the test to assert that
both mockReviewer instances are invoked exactly once, since ReviewProvider calls
are launched unconditionally via errgroup.Go and m2’s error should not prevent
m1 from running. Use the existing mockReviewer.callCount checks in the test to
make the expectation explicit and meaningful.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/model/multi_test.go`:
- Around line 205-217: The assertion in TestMultiProviderReview_PartialError is
too weak and does not verify the deterministic behavior of MultiProvider.Review.
Update the test to assert that both mockReviewer instances are invoked exactly
once, since ReviewProvider calls are launched unconditionally via errgroup.Go
and m2’s error should not prevent m1 from running. Use the existing
mockReviewer.callCount checks in the test to make the expectation explicit and
meaningful.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4dd3f164-4be2-4d0d-98bc-97801c05159d

📥 Commits

Reviewing files that changed from the base of the PR and between 88eb6bb and 97cf14f.

📒 Files selected for processing (4)
  • internal/config/config.go
  • internal/model/multi.go
  • internal/model/multi_test.go
  • internal/model/parse_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/config/config.go
  • internal/model/multi.go

…g dedup

Multi-model consensus mode runs 2+ AI models in parallel and only keeps
findings that multiple models agree on, reducing false positives.

New files:
- internal/model/multi.go: MultiProvider with errgroup-based parallel
  execution, finding dedup (file+line±3+category), consensus threshold

Config:
- --models flag: comma-separated model list (e.g. gemini-2.5-flash,claude-sonnet-4)
- --consensus-threshold flag: min models that must agree (default: 2)
- REVIEW_MODELS env var support

Wiring:
- cmd/code-reviewer/main.go: auto-creates MultiProvider when len(models) > 1

Tests (8 new):
- findingsMatch: boundary cases for line proximity and category matching
- mergeResults: threshold filtering, all-agree, single model, nil results,
  summary dedup, high threshold, multi-finding dedup

112 tests total, all passing with -race.
…rehensive tests

Bug Fixes:
- CRITICAL: Fixed transitive chain in mergeResults — canonical finding's
  line shift could merge unrelated findings 5+ lines apart. Added stable
  'anchor' field that never changes after group creation.
- Extracted ReviewProvider interface from []*Provider for testability
  and correct resource cleanup via Close()

New Tests (15 added, 127 total):
- Transitive drift regression: L10→L12→L15 chain correctly rejected
- MultiProvider.Review concurrent execution with mock providers
- Partial model error propagation
- MultiProvider.Close verification
- parseReviewJSON: markdown fences (json/JSON/bare), surrounding text,
  nested braces, malformed input, empty string, whitespace wrapping

Refactoring:
- splitAndTrim helper in config.go (DRYs loadEnv/loadFlags)
- NewMultiProviderFromReviewers constructor for test injection

Coverage:
- internal/model: 62.7% → 73.8%
- config: 75.1% → 76.7%
@brucearctor
brucearctor force-pushed the feat/multi-model-consensus branch from 97cf14f to bcac196 Compare July 9, 2026 04:22
@brucearctor
brucearctor merged commit 1e36c74 into main Jul 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant