Skip to content

feat(review): route trivial diffs to a cheaper model - #6590

Draft
guyoron1 wants to merge 1 commit into
fullsend-ai:mainfrom
guyoron1:feat/review-complexity-model-routing
Draft

feat(review): route trivial diffs to a cheaper model#6590
guyoron1 wants to merge 1 commit into
fullsend-ai:mainfrom
guyoron1:feat/review-complexity-model-routing

Conversation

@guyoron1

Copy link
Copy Markdown
Contributor

Closes #5777.

Author type is a proxy for complexity, not complexity itself. The harness fixes the review model before anything has looked at the diff, so the one-line versions.json bump in #5777 — human-authored, created through the web UI — was reviewed on Opus for $0.40 and correctly approved in about a minute. The verdict was right; the price was not.

The existing cheap-review proposals (#2842, #2639, #3240, #3347) all gate on bot authorship, so none of them would have caught that PR. #1900 covers human-authored changes but only for docs/prompt-only Markdown. This routes on the diff instead, which catches both.

What qualifies

A step ahead of the review agent classifies the changed files and, when the diff is provably trivial, exports FULLSEND_MODEL so the review runs on a cheaper tier. It lives in the dispatch layer, so it applies to every enrolled repo without a per-repo harness change.

Trivial means every changed file is a modified data/config file (.json, .ya?ml, .toml, .txt, .md, .ini, .cfg, .conf, .properties) and the whole diff is under the line threshold.

The exclusions are where the safety lives:

Excluded Why
added / removed / renamed files structural, not a value edit — even at one line
any code file never trivial, whatever its size
dependency lockfiles npm resolves from the lockfile, so a one-line integrity swap is a supply-chain change wearing a trivial diff
paths that execute or govern dot directories (.github/, .claude/), scripts/, hack/, skills/, harness/, agents/, policies/, and root files like CODEOWNERS, Dockerfile, CLAUDE.md

That last list is deliberately smaller than REVIEW_PROTECTED_PATHS. The two answer different questions: protected paths decide whether a review may auto-approve; this decides whether a cheaper model may form the opinion in the first place.

Tuning and precedence

The issue is explicit that the thresholds need tuning, so both are repository variables:

Variable Default Meaning
REVIEW_TRIVIAL_MODEL sonnet Model for trivial diffs. off disables routing entirely.
REVIEW_TRIVIAL_MAX_LINES 10 Upper bound on additions + deletions.

Routing runs before setup-agent-env.sh, which re-exports FULLSEND_MODEL from the REVIEW_FULLSEND_MODEL / FULLSEND_MODEL repository variables. A repo that pins a model therefore keeps it — explicit configuration outranks inferred routing, and the ordering is what enforces that rather than a special case in the script.

Failure mode

Every uncertain path leaves the model untouched, so the harness default (the more capable model) stands: an unreadable file list, a malformed threshold, a truncated listing (GitHub caps the files endpoint at 3000 and stops paginating without erroring), an empty diff, a missing PR number. The failure mode of cost routing must be a more expensive review, never a missing one.

Testing

.github/scripts/route-review-model-test.sh stubs gh and runs the real script — not a paraphrase of its logic — across 16 cases, wired into make script-test. It covers the issue's own example, each exclusion class, threshold accumulation across files, both repository variables, the opt-out, and every degrade-safely path.

shellcheck and actionlint are clean; I diffed actionlint against pristine origin/main and this adds zero new findings (the 31 existing ones are the job.workflow_sha context actionlint doesn't know). go test ./internal/scaffold/ passes, which covers the cross-workflow alignment since the step is mirrored into reusable-review.yml as well.

On the validation criteria

Criteria 1, 2 and 4 are observable once this runs. Criterion 3 (≥50% cost drop) is worth measuring rather than assuming — the $0.40 baseline is a single data point from July, and per-dimension model tiering has landed since. The review agent's own metrics.json already records per-run cost, so the number is available from the next trivial PR that goes through this path.

Author type is a proxy for complexity, not complexity itself. A one-line
human edit to versions.json runs on the same model as a refactor, because
the harness fixes the model before anything has looked at the diff — the
$0.40 approval in fullsend-ai#5777.

A step ahead of the review agent classifies the diff and, when it is
provably trivial, exports FULLSEND_MODEL so the review runs on a cheaper
tier. It lives in the dispatch layer rather than a harness, so it applies
to every enrolled repo without a per-repo change, and it keys on the diff
rather than the author, so it catches human and bot PRs alike.

Trivial means every changed file is a modified data/config file and the
whole diff is under the threshold. The exclusions carry the safety:
added, removed and renamed files are structural even when small; code
never qualifies whatever its size; dependency lockfiles never qualify,
because npm resolves from the lockfile and a one-line integrity swap is a
supply-chain change wearing a trivial diff; and paths that execute or
govern never qualify — a deliberately smaller list than
REVIEW_PROTECTED_PATHS, which decides whether a review may auto-approve
where this decides whether a cheaper model may form the opinion.

Two repository variables tune it, since the issue is explicit that the
thresholds need tuning: REVIEW_TRIVIAL_MODEL (default sonnet, 'off'
disables) and REVIEW_TRIVIAL_MAX_LINES (default 10). Routing runs before
setup-agent-env.sh, so an explicit REVIEW_FULLSEND_MODEL or FULLSEND_MODEL
repository variable overwrites it — configuration outranks inference.

Every uncertain path leaves the model untouched: an unreadable file list,
a malformed threshold, a truncated listing, an empty diff. The failure
mode of cost routing must be a more expensive review, never a missing
one.

Signed-off-by: guy oron <goron@redhat.com>
@github-actions

Copy link
Copy Markdown

E2E tests did not run

E2E tests run automatically for org/repo members and collaborators on pull requests.

For other contributors, a maintainer must add the ok-to-test label after the latest push.

See E2E testing guide for details.

@github-actions

Copy link
Copy Markdown

Site preview

Preview: https://3f87bc3c-site.fullsend-ai.workers.dev

Commit: f9b960e06a2a0d423dd698d8b2e77f6358a7583b

@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Route trivial review diffs to a cheaper model

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Classifies pull request diffs and routes only provably trivial value edits to cheaper review
 models.
• Preserves pinned model precedence and fails open to the existing capable model.
• Adds reusable workflow integration, classifier tests, configuration controls, and runtime
 documentation.
Diagram

graph TD
  A["Review workflow"] --> B["GitHub files API"] --> C{"Trivial diff?"}
  C -- "Yes" --> D["Export cheap model"] --> E["Agent setup"] --> F["Review agent"]
  C -- "No or uncertain" --> E
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Classify inside the review agent
  • ➕ Could use richer semantic context than file metadata
  • ➕ Could evolve beyond extension and path heuristics
  • ➖ The expensive model is already selected before the agent examines the diff
  • ➖ Adds model cost and latency before routing can occur
2. Route by author or automation label
  • ➕ Simple eligibility checks
  • ➕ Avoids maintaining file classification rules
  • ➖ Misses trivial human-authored changes
  • ➖ Treats authorship as an unreliable proxy for complexity
  • ➖ May under-review structurally significant bot changes
3. Embed classification in each workflow
  • ➕ Avoids invoking a separate shell script
  • ➕ Keeps routing visible beside each review job
  • ➖ Duplicates safety logic across workflows
  • ➖ Makes tests and future rule changes more difficult
  • ➖ Increases the risk of dispatch and direct-review behavior diverging

Recommendation: Keep the shared dispatch-layer classifier. It makes the decision before model initialization, applies equally to human and automated changes, centralizes conservative exclusions, and fails safely to the existing model. The agent-based alternative offers richer analysis but cannot deliver the same pre-execution cost reduction.

Files changed (6) +293 / -0

Enhancement (1) +127 / -0
route-review-model.shClassify trivial diffs and export a cheaper review model +127/-0

Classify trivial diffs and export a cheaper review model

• Fetches PR file metadata and uses a single jq classifier to require modified data/config files below the configured line threshold. Excludes lockfiles, executable or governing paths, structural changes, and truncated or uncertain results while always leaving the default model intact on failure.

.github/scripts/route-review-model.sh

Tests (2) +116 / -0
route-review-model-test.shAdd end-to-end shell tests for trivial-diff routing +115/-0

Add end-to-end shell tests for trivial-diff routing

• Stubs the GitHub CLI and executes the production classifier against controlled file lists. Covers qualifying data edits, guarded paths, code, lockfiles, structural changes, aggregate thresholds, configuration overrides, and fail-open behavior.

.github/scripts/route-review-model-test.sh

MakefileRegister model-routing tests in the script test suite +1/-0

Register model-routing tests in the script test suite

• Adds the new shell test script to the existing timed 'script-test' target.

Makefile

Documentation (1) +22 / -0
runtimes.mdDocument trivial-diff model routing and precedence +22/-0

Document trivial-diff model routing and precedence

• Explains eligibility and safety exclusions, documents the configurable model and line threshold, and clarifies explicit model precedence and fail-open behavior.

docs/runtimes.md

Other (2) +28 / -0
reusable-dispatch.ymlRoute trivial dispatch reviews before agent setup +14/-0

Route trivial dispatch reviews before agent setup

• Invokes the model-routing script in the shared dispatch review path with repository-configurable model and threshold values. Running it before agent setup ensures explicit repository model settings retain precedence.

.github/workflows/reusable-dispatch.yml

reusable-review.ymlRoute trivial direct reviews before agent setup +14/-0

Route trivial direct reviews before agent setup

• Adds the same pre-setup model-routing step to the reusable review workflow. It derives the PR number from the event payload and passes repository routing variables to the classifier.

.github/workflows/reusable-review.yml

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. PR files query misprojects 🐞 Bug ≡ Correctness
Description
The files endpoint returns arrays, but --jq '{filename, status, changes}' projects fields from
each page array instead of iterating .[], producing null-valued records that are always blocked.
The -F per_page=100 field also needs an explicit GET method or a query parameter; otherwise
routing fails open and never selects the cheaper model.
Code

.github/scripts/route-review-model.sh[R86-87]

+if ! FILES_JSON=$(gh api "repos/${SOURCE_REPO}/pulls/${PR_NUMBER}/files" \
+  --paginate -F per_page=100 --jq '{filename, status, changes}' 2>/dev/null); then
Relevance

●●● Strong

Established precedent explicitly requires iterating PR-files API arrays and handling fetch failures
safely.

PR-#2398

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The classifier requires individual records with non-null status and filename fields, while
established use of the same endpoint explicitly iterates .[].filename. The test stub directly
emits newline-delimited objects and therefore masks the production response-shape error.

.github/scripts/route-review-model.sh[70-82]
.github/workflows/e2e.yml[133-138]
.github/scripts/route-review-model-test.sh[25-26]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The production GitHub API invocation does not emit one `{filename, status, changes}` object per changed file. This prevents trivial PRs from being routed even though the tests pass.

## Issue Context
The pull-request files endpoint returns page arrays. Existing repository usage iterates those arrays with `.[]`, while the new test stub bypasses the real response shape and ignores command arguments.

## Fix Focus Areas
- .github/scripts/route-review-model.sh[86-87]
- .github/scripts/route-review-model-test.sh[25-26]
- .github/workflows/e2e.yml[133-138]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Governing configs route cheaply 🐞 Bug ≡ Correctness
Description
is_guarded omits root dotfiles and unlisted root manifests, so small edits to executable or
policy-bearing files such as action.yml, .goreleaser.yml, .pre-commit-config.yaml, and
renovate.json qualify as trivial. This violates the routing safety rule that paths which execute
or govern must never use the cheaper tier.
Code

.github/scripts/route-review-model.sh[R68-69]

+def is_guarded: test("(^|/)\\.[^/]+/") or test("^(scripts|hack|agents|skills|harness|images|plugins|policies|profiles|providers|api-servers)/")
+                or test("^(CODEOWNERS|AGENTS\\.md|CLAUDE\\.md|Dockerfile|Containerfile)$");
Relevance

●●● Strong

Safety-critical classifier gap; accepted precedents favor guarding executable and policy-bearing
paths.

PR-#5743
PR-#6696

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The classifier accepts YAML and JSON and blocks only the narrow guarded regex. action.yml defines
executable composite steps, renovate.json enables dependency automerging, and the repository's
root dot-config files define release, hook, and CI policies, yet none match the new guarded
alternatives.

.github/scripts/route-review-model.sh[65-80]
action.yml[70-79]
renovate.json[1-17]
.pre-commit-config.yaml[1-18]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The guarded-path matcher is an incomplete denylist and allows multiple executable, release, validation, and dependency-automation configurations to be classified as trivial.

## Issue Context
The accepted extension list includes YAML and JSON. Root hidden files do not match the hidden-directory pattern because it requires a trailing slash, and operational manifests such as `action.yml` and `renovate.json` are absent from the root-file alternatives.

## Fix Focus Areas
- .github/scripts/route-review-model.sh[65-80]
- .github/scripts/route-review-model-test.sh[54-82]
- action.yml[70-79]
- renovate.json[1-17]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Pinned model override ignored 🐞 Bug ≡ Correctness
Description
reusable-review.yml now writes the inferred FULLSEND_MODEL, but its subsequent setup step does
not receive FULLSEND_REPO_VARS, so REVIEW_FULLSEND_MODEL or FULLSEND_MODEL repository pins
cannot overwrite it. Repositories using this workflow can therefore run reviews on the cheap model
despite an explicit model selection.
Code

.github/workflows/reusable-review.yml[R163-167]

+      # Complexity-based model routing (#5777). Runs before the setup step
+      # below on purpose: setup-agent-env.sh re-exports FULLSEND_MODEL from
+      # the REVIEW_FULLSEND_MODEL / FULLSEND_MODEL repository variables, so a
+      # repo that pins a model keeps it — explicit configuration outranks an
+      # inferred route.
Relevance

●●● Strong

Reusable workflow secret and environment forwarding omissions are consistently accepted when they
break configuration propagation.

PR-#3903
PR-#5502

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The setup script gates all repository-variable override processing on FULLSEND_REPO_VARS. The
dispatch integration supplies that map, whereas the reusable review setup environment omits it
immediately after the new routing step.

.github/workflows/reusable-review.yml[168-183]
.github/workflows/reusable-dispatch.yml[927-936]
internal/scaffold/fullsend-repo/.github/scripts/setup-agent-env.sh[41-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The reusable review workflow activates inferred model routing without passing repository variables to the setup script that is supposed to restore explicit model pins.

## Issue Context
`setup-agent-env.sh` only processes model overrides when `FULLSEND_REPO_VARS` is populated. The dispatch workflow passes `${{ toJSON(vars) }}`, but the reusable review workflow does not.

## Fix Focus Areas
- .github/workflows/reusable-review.yml[163-183]
- .github/workflows/reusable-dispatch.yml[927-936]
- internal/scaffold/fullsend-repo/.github/scripts/setup-agent-env.sh[41-56]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. gh api bypasses forge abstraction 📘 Rule violation ⌂ Architecture
Description
The new classifier directly invokes gh api to retrieve pull-request files instead of routing the
operation through forge.Client. This couples dispatch routing to GitHub and places a direct GitHub
API call outside internal/forge/github/.
Code

.github/scripts/route-review-model.sh[R86-87]

+if ! FILES_JSON=$(gh api "repos/${SOURCE_REPO}/pulls/${PR_NUMBER}/files" \
+  --paginate -F per_page=100 --jq '{filename, status, changes}' 2>/dev/null); then
Relevance

● Weak

Recent precedent rejected moving direct gh API calls into forge abstraction for workflow
compatibility.

PR-#5578

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rules 1062052 and 1062054 require forge operations to use forge.Client and restrict direct GitHub
API calls to internal/forge/github/. The changed script calls the GitHub pull-files endpoint
through gh api, while the repository already defines pull-request file retrieval on forge.Client
with its GitHub implementation under the required package.

Rule 1062052: Route all git forge operations through forge.Client
Rule 1062054: Restrict direct GitHub API calls to internal/forge/github
.github/scripts/route-review-model.sh[86-87]
internal/forge/forge.go[643-652]
internal/forge/github/github.go[2953-2975]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The review-model classifier directly retrieves pull-request files through `gh api`, bypassing the repository's forge abstraction.

## Issue Context
All forge operations must flow through `forge.Client`, and GitHub-specific API implementation belongs under `internal/forge/github/`. Extend or reuse the forge interface to return the filename, status, and change count required by the classifier, then invoke that abstraction from the dispatch layer and update its tests.

## Fix Focus Areas
- .github/scripts/route-review-model.sh[86-87]
- .github/scripts/route-review-model-test.sh[25-33]
- internal/forge/forge.go[643-652]
- internal/forge/github/github.go[2953-2975]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 65 rules
Review mode: ⚖️ Balanced: This adds security- and cost-sensitive model-routing logic across shell scripts and reusable CI workflows, with multiple edge cases and precedence assumptions requiring a careful review.

Grey Divider

Tip of the day
💡 Did you know, you can describe a rule in plain language on the Rules page and Qodo drafts it for you

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +86 to +87
if ! FILES_JSON=$(gh api "repos/${SOURCE_REPO}/pulls/${PR_NUMBER}/files" \
--paginate -F per_page=100 --jq '{filename, status, changes}' 2>/dev/null); then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Pr files query misprojects 🐞 Bug ≡ Correctness

The files endpoint returns arrays, but --jq '{filename, status, changes}' projects fields from
each page array instead of iterating .[], producing null-valued records that are always blocked.
The -F per_page=100 field also needs an explicit GET method or a query parameter; otherwise
routing fails open and never selects the cheaper model.
Agent Prompt
## Issue description
The production GitHub API invocation does not emit one `{filename, status, changes}` object per changed file. This prevents trivial PRs from being routed even though the tests pass.

## Issue Context
The pull-request files endpoint returns page arrays. Existing repository usage iterates those arrays with `.[]`, while the new test stub bypasses the real response shape and ignores command arguments.

## Fix Focus Areas
- .github/scripts/route-review-model.sh[86-87]
- .github/scripts/route-review-model-test.sh[25-26]
- .github/workflows/e2e.yml[133-138]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +68 to +69
def is_guarded: test("(^|/)\\.[^/]+/") or test("^(scripts|hack|agents|skills|harness|images|plugins|policies|profiles|providers|api-servers)/")
or test("^(CODEOWNERS|AGENTS\\.md|CLAUDE\\.md|Dockerfile|Containerfile)$");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Governing configs route cheaply 🐞 Bug ≡ Correctness

is_guarded omits root dotfiles and unlisted root manifests, so small edits to executable or
policy-bearing files such as action.yml, .goreleaser.yml, .pre-commit-config.yaml, and
renovate.json qualify as trivial. This violates the routing safety rule that paths which execute
or govern must never use the cheaper tier.
Agent Prompt
## Issue description
The guarded-path matcher is an incomplete denylist and allows multiple executable, release, validation, and dependency-automation configurations to be classified as trivial.

## Issue Context
The accepted extension list includes YAML and JSON. Root hidden files do not match the hidden-directory pattern because it requires a trailing slash, and operational manifests such as `action.yml` and `renovate.json` are absent from the root-file alternatives.

## Fix Focus Areas
- .github/scripts/route-review-model.sh[65-80]
- .github/scripts/route-review-model-test.sh[54-82]
- action.yml[70-79]
- renovate.json[1-17]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +163 to +167
# Complexity-based model routing (#5777). Runs before the setup step
# below on purpose: setup-agent-env.sh re-exports FULLSEND_MODEL from
# the REVIEW_FULLSEND_MODEL / FULLSEND_MODEL repository variables, so a
# repo that pins a model keeps it — explicit configuration outranks an
# inferred route.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Pinned model override ignored 🐞 Bug ≡ Correctness

reusable-review.yml now writes the inferred FULLSEND_MODEL, but its subsequent setup step does
not receive FULLSEND_REPO_VARS, so REVIEW_FULLSEND_MODEL or FULLSEND_MODEL repository pins
cannot overwrite it. Repositories using this workflow can therefore run reviews on the cheap model
despite an explicit model selection.
Agent Prompt
## Issue description
The reusable review workflow activates inferred model routing without passing repository variables to the setup script that is supposed to restore explicit model pins.

## Issue Context
`setup-agent-env.sh` only processes model overrides when `FULLSEND_REPO_VARS` is populated. The dispatch workflow passes `${{ toJSON(vars) }}`, but the reusable review workflow does not.

## Fix Focus Areas
- .github/workflows/reusable-review.yml[163-183]
- .github/workflows/reusable-dispatch.yml[927-936]
- internal/scaffold/fullsend-repo/.github/scripts/setup-agent-env.sh[41-56]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@guyoron1
guyoron1 marked this pull request as draft September 1, 2026 12:15
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.

Review model routing should use diff complexity, not just author type

1 participant