fix(review): drop fixed per-tier output caps; reviews use the route's output limit (Refs #7067, CW-7525)

Review v2 slice F1. budget_for() no longer hard-codes 4,000 / 8,000 /
16,000 output tokens for Low / Medium / High risk. It takes the route's
effective output limit (client.effective_max_output_tokens) and every
review pass may use all of it, so a review is never truncated by a tier
cap. Tiers still set steps and tool calls; the money cap, step budget and
tool budget keep stopping a review early as a named partial review
(skip, not truncate).

with_reasoning_floor() is deleted: it only existed to lift the tier cap
to 8,192 for reasoning routes, and the route limit already is at least
that. Both callers (`codewhale review` in lib.rs and the review tool)
delete their `.min(budget.max_output_tokens)`; the allowance they send,
the reserve and the exhausted-pass message all read
budget.max_output_tokens, which is the value actually sent. The
"numbers that were never sent" error was already fixed by 2d158d78b1;
this keeps the reported and sent values the same source.

The review_budget receipt field max_output_tokens now records the real
route allowance instead of the tier cap.

CHANGELOG: rewrote the Unreleased #7067 entry, which described the
removed 8,192 floor.

Verification: scripts/dev-test.sh tui review_triage tools::review
dependencies:: root_operate_dispatch_delegates_builtin_verification_but_not_shell
-> 142 tests run: 142 passed (86 tools::review incl. 6 review_triage,
55 dependencies::, 1 subagent). No live provider review was run.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-27 09:08:05 -07:00
1 parent e66998bec3
commit 8084ead09c
5 files changed
+44 -49

No files matched your search

+8 -6
View File
@@ -110,12 +110,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
unchanged (#6436, #7082).
- `codewhale review` no longer accepts leaked tool-call markup as a review: a
pass whose reply is a DeepSeek DSML (or other) tool-call wrapper outside code
now fails as incomplete, with no tally and nothing posted (#7072). A review on a
reasoning model now raises its tier output allowance to 8,192 tokens (a
Low-risk review used to get 4,000, which a reasoning pass could spend entirely
on thinking), the `review` tool bounds its reasoning level to leave room for
the review, and an exhausted pass reports the allowance and reserve actually
sent and says "cut off" when visible text came back (#7067).
now fails as incomplete, with no tally and nothing posted (#7072). Reviews no
longer have fixed per-risk output caps (a Low-risk review used to get 4,000
tokens, which a reasoning pass could spend entirely on thinking): every pass
may use the route's own output limit, and the step, tool and money budgets
still stop a review early as a named partial review rather than truncating
it. The `review` tool bounds its reasoning level to leave room for the review,
and an exhausted pass reports the allowance and reserve actually sent and
says "cut off" when visible text came back (#7067).
- `--full-access` is now the documented flag for Full Access (`--yolo` stays an
alias), and `serve` accepts it after the subcommand: `codewhale serve --acp
--yolo` used to fail with "unexpected argument". `serve --acp` also writes one
+8 -6
View File
@@ -110,12 +110,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
unchanged (#6436, #7082).
- `codewhale review` no longer accepts leaked tool-call markup as a review: a
pass whose reply is a DeepSeek DSML (or other) tool-call wrapper outside code
now fails as incomplete, with no tally and nothing posted (#7072). A review on a
reasoning model now raises its tier output allowance to 8,192 tokens (a
Low-risk review used to get 4,000, which a reasoning pass could spend entirely
on thinking), the `review` tool bounds its reasoning level to leave room for
the review, and an exhausted pass reports the allowance and reserve actually
sent and says "cut off" when visible text came back (#7067).
now fails as incomplete, with no tally and nothing posted (#7072). Reviews no
longer have fixed per-risk output caps (a Low-risk review used to get 4,000
tokens, which a reasoning pass could spend entirely on thinking): every pass
may use the route's own output limit, and the step, tool and money budgets
still stop a review early as a named partial review rather than truncating
it. The `review` tool bounds its reasoning level to leave room for the review,
and an exhausted pass reports the allowance and reserve actually sent and
says "cut off" when visible text came back (#7067).
- `--full-access` is now the documented flag for Full Access (`--yolo` stays an
alias), and `serve` accepts it after the subcommand: `codewhale serve --acp
--yolo` used to fail with "unexpected argument". `serve --acp` also writes one
+9 -9
View File
@@ -10526,15 +10526,15 @@ async fn run_review(config: &Config, args: ReviewArgs) -> Result<()> {
// text, so the review request has to keep room for the review itself.
let review_reserve_percent =
crate::route_budget::review_visible_text_reserve_percent(&request_route.model);
let review_budget = crate::tools::review_triage::budget_for(&review_triage)
.with_reasoning_floor(review_reserve_percent)
.with_money_cap(
args.money_cap_usd,
review_rates.map(|rates| (rates.input_usd_per_million, rates.output_usd_per_million)),
);
let review_allowance = client
.effective_max_output_tokens(&request_route.model)
.min(review_budget.max_output_tokens);
let review_budget = crate::tools::review_triage::budget_for(
&review_triage,
client.effective_max_output_tokens(&request_route.model),
)
.with_money_cap(
args.money_cap_usd,
review_rates.map(|rates| (rates.input_usd_per_million, rates.output_usd_per_million)),
);
let review_allowance = review_budget.max_output_tokens;
let review_reserve_tokens = crate::route_budget::review_visible_text_reserve_tokens(
&request_route.model,
review_allowance,
+5 -5
View File
@@ -1576,11 +1576,11 @@ impl ToolSpec for ReviewTool {
let triage = super::review_triage::triage_review(&review_diff);
let reserve_percent =
crate::route_budget::review_visible_text_reserve_percent(&route.model);
let budget =
super::review_triage::budget_for(&triage).with_reasoning_floor(reserve_percent);
let allowance = client
.effective_max_output_tokens(&route.model)
.min(budget.max_output_tokens);
let budget = super::review_triage::budget_for(
&triage,
client.effective_max_output_tokens(&route.model),
);
let allowance = budget.max_output_tokens;
let reasoning_effort =
(codewhale_models::model_reasoning_capability(&route.model) == Some(true)).then(|| {
bounded_review_reasoning_effort(
+14 -23
View File
@@ -185,19 +185,20 @@ pub const REASONING_ROUTE_MIN_OUTPUT_TOKENS: u32 = 8_192;
/// The budget a tier buys. Low risk still gets a real review of the diff, just
/// without repository tools; high risk gets dependent tracing and room to
/// reproduce.
/// reproduce. Output is never tiered: every review may use the route's whole
/// output limit, so spend is bounded by skipping steps, not by truncation.
#[must_use]
pub fn budget_for(triage: &ReviewTriage) -> ReviewBudget {
let (max_steps, max_tool_calls, max_output_tokens) = match triage.risk {
ReviewRisk::Low => (1, 0, 4_000),
ReviewRisk::Medium => (4, 6, 8_000),
ReviewRisk::High => (6, 12, 16_000),
pub fn budget_for(triage: &ReviewTriage, route_output_limit: u32) -> ReviewBudget {
let (max_steps, max_tool_calls) = match triage.risk {
ReviewRisk::Low => (1, 0),
ReviewRisk::Medium => (4, 6),
ReviewRisk::High => (6, 12),
};
ReviewBudget {
risk: triage.risk,
max_steps,
max_tool_calls,
max_output_tokens,
max_output_tokens: route_output_limit,
money_cap_usd: None,
money_limit_supported: false,
}
@@ -213,16 +214,6 @@ impl ReviewBudget {
}
}
#[must_use]
pub fn with_reasoning_floor(mut self, reserve_percent: u32) -> Self {
if reserve_percent > 0 {
self.max_output_tokens = self
.max_output_tokens
.max(REASONING_ROUTE_MIN_OUTPUT_TOKENS);
}
self
}
/// Attach a per-review money cap. A cap without a price is not enforceable,
/// and saying so is the point: `money_limit_supported` stays false until a
/// rate is supplied, so a caller can never report a cap it did not apply.
@@ -331,7 +322,7 @@ mod tests {
"diff --git a/docs/README.md b/docs/README.md\n--- a/docs/README.md\n+++ b/docs/README.md\n@@ -1 +1 @@\n-old\n+new\n",
);
assert_eq!(triage.risk, ReviewRisk::Low);
let budget = budget_for(&triage);
let budget = budget_for(&triage, 65_536);
assert_eq!(budget.max_tool_calls, 0);
assert_eq!(budget.max_steps, 1);
assert!(!budget.money_limit_supported);
@@ -353,7 +344,7 @@ mod tests {
.iter()
.any(|reason| reason.contains("migration"))
);
let budget = budget_for(&triage);
let budget = budget_for(&triage, 65_536);
assert!(budget.max_tool_calls >= 12);
assert!(budget.max_steps >= 6);
}
@@ -383,12 +374,12 @@ mod tests {
#[test]
fn a_money_cap_without_a_price_is_reported_as_unsupported() {
let triage = triage_review(&diff_for(&["src/auth.rs"]));
let without_rates = budget_for(&triage).with_money_cap(Some(0.25), None);
let without_rates = budget_for(&triage, 65_536).with_money_cap(Some(0.25), None);
assert_eq!(without_rates.money_cap_usd, Some(0.25));
assert!(!without_rates.money_limit_supported);
let with_rates = budget_for(&triage).with_money_cap(Some(0.25), Some((0.15, 0.60)));
let with_rates = budget_for(&triage, 65_536).with_money_cap(Some(0.25), Some((0.15, 0.60)));
assert!(with_rates.money_limit_supported);
let no_cap = budget_for(&triage).with_money_cap(None, Some((0.15, 0.60)));
let no_cap = budget_for(&triage, 65_536).with_money_cap(None, Some((0.15, 0.60)));
assert_eq!(no_cap.money_cap_usd, None);
assert!(!no_cap.money_limit_supported);
}
@@ -412,7 +403,7 @@ mod tests {
#[test]
fn a_partial_review_names_the_stop_and_the_limit() {
let triage = triage_review(&diff_for(&["src/auth.rs"]));
let budget = budget_for(&triage).with_money_cap(Some(0.25), Some((0.15, 0.60)));
let budget = budget_for(&triage, 65_536).with_money_cap(Some(0.25), Some((0.15, 0.60)));
let note = partial_review_note(&ReviewStop::MoneyCap, &triage, &budget);
assert!(note.contains("named partial review"));
assert!(note.contains("0.2500"));