feat(config): cap reviewer effort per model tier - #552
Conversation
Agent catalogs declare an absolute effort that becomes the provider's
reasoning-effort setting. A deployment that wants to bound spend on an
expensive tier previously had only one lever: editing the shared catalog,
which changes the agent's declared intent for every consumer.
Add llm.max_effort, a per-tier ceiling resolved alongside model_map:
max_effort:
large: medium
A tier absent from the map is uncapped. The cap is a ceiling only, so an
agent declaring low under a medium ceiling still runs at low.
The clamp lives in stagemodel.ResolveStageModel, the single documented path
from profile preferences to a concrete model and effort, so every stage picks
it up without per-stage changes.
Reviewer resolution previously discarded the resolver's effort and passed
agent.Effort straight through, which would have left reviewers - the only
path that reaches the large tier - silently uncapped. The resolved effort is
now threaded through reviewerRuntimeResolution.
cr init has no editor for max_effort, so the runtime round trip is extended
to preserve it: dropping the field would silently discard a hand-written
ceiling on any later init pass. identityKey now includes the map so two
runtimes differing only by ceiling no longer collide.
Four paths intentionally bypass the ceiling because each is an explicit
selection of a concrete model or effort: --reviewer-effort, --reviewer-model,
agent model_id, and benchmark suites, where stages.reviewers.effort is
required so candidates stay comparable. README and docs/architecture.md name
all four.
PR #552 — pre-fix TDD assessmentBlockerNo findings. Major
Minor
Nit
|
PR #552 post-fix TDD / coverage assessmentAssessed at Verdict: PASS, with one targeted non-blocking test gapThe resolver is the single precedence point, and the focused suite passed: P2 — worthwhile follow-upAdd one dry-run pipeline case with a configured ceiling, Covered convincingly
Low-value expansion to skipDo not add a full benchmark subprocess/config-ceiling matrix: benchmark code only |
There was a problem hiding this comment.
Automated PR Review
Reviewed commit: ba724ecef2a2
Profile: codex-rianjs-bot - Posting as: rianjs-bot[bot]
Summary
| Reviewer | Findings |
|---|---|
| go:implementation-tests | 1 |
| policies:conventions | 1 |
| structure:repo-health | 1 |
go:implementation-tests (1 finding)
Major - internal/stagemodel/resolver.go:97
Applying
EffortOverrideafterapplyMaxEffortmakes--selection-effortbypassllm.max_effort. That override is routed through this resolver but is not among the documented bypasses, so a deployment that caps the medium tier atlowcan still run selection athigh(the new test explicitly codifies that escape). Apply the override before clamping, or clamp the override as well; retain the reviewer-specific post-resolution override only if that explicit escape hatch is intended.
policies:conventions (1 finding)
Minor - docs/architecture.md:61
The new ceiling is part of the documented single resolver boundary, but the existing architecture guardrail only rejects direct
config.ResolveModelTiercalls. It does not cover the newly addedconfig.ResolveMaxEffort, so a future runtime stage can apply or bypass the ceiling outsideinternal/stagemodelwithout the check catching it. Extendinternal/architecture/model_resolution_test.goto reject direct calls to both resolver helpers outside its approved packages.
structure:repo-health (1 finding)
Minor - internal/view/config.go:270
max_effortis rendered only as a text suffix;ModelMapRow—used bycr config llm models list --json—has no ceiling field. Programmatic config inspection therefore cannot determine the effective effort constraint for a returned tier. Add a JSONmax_effortfield toModelMapRow, populate it from the normalized/effective configuration, and cover both capped and uncapped JSON rows.
Reviewer Coverage
go:implementation-tests— complete (broad); inspected 15 assigned files (18 inspected across reviewers):internal/cmd/initcmd/initcmd.go,internal/cmd/initcmd/initcmd_max_effort_test.go,internal/cmd/initcmd/initcmd_test.go,internal/config/config.go,internal/config/config_max_effort_test.go,internal/modelprefs/modelprefs.go,internal/modelprefs/modelprefs_effort_test.go,internal/pipeline/artifacts.go,internal/pipeline/pipeline.go,internal/pipeline/pipeline_test.go,internal/pipeline/prompts.go,internal/stagemodel/resolver.go,internal/stagemodel/resolver_test.go,internal/view/config.go,internal/view/config_test.go; skipped: none; constraints: Review scope limited to assigned Go implementation and tests. Regular CGO-enabled tests cannot compile from the workspace path because clang mishandles its spaces; CGO-disabled focused tests passed for config, modelprefs, and stagemodel.policies:conventions— complete (broad); inspected 7 assigned files (18 inspected across reviewers):README.md,docs/architecture.md,docs/init-config-surface.md,internal/cmd/initcmd/initcmd.go,internal/config/config.go,internal/pipeline/pipeline.go,internal/view/config.go; skipped: none; constraints: Shared CLI standards were not available as a local convenience copy; review used repo-local conventions and the supplied review context. Targeted Go tests could not build because the sandbox worktree path contains spaces that clang treated as split path arguments.structure:repo-health— complete (broad); inspected 9 assigned files (18 inspected across reviewers):docs/architecture.md,internal/cmd/initcmd/initcmd.go,internal/config/config.go,internal/modelprefs/modelprefs.go,internal/pipeline/artifacts.go,internal/pipeline/pipeline.go,internal/pipeline/prompts.go,internal/stagemodel/resolver.go,internal/view/config.go; skipped: none; constraints: Review limited to the nine assigned files and their directly relevant tests/guardrail. Targeted Go tests could not build because the sandboxed workspace path contains spaces that the C toolchain treated as split paths.
Inspected files (18)
README.mddocs/architecture.mddocs/init-config-surface.mdinternal/cmd/initcmd/initcmd.gointernal/cmd/initcmd/initcmd_max_effort_test.gointernal/cmd/initcmd/initcmd_test.gointernal/config/config.gointernal/config/config_max_effort_test.gointernal/modelprefs/modelprefs.gointernal/modelprefs/modelprefs_effort_test.gointernal/pipeline/artifacts.gointernal/pipeline/pipeline.gointernal/pipeline/pipeline_test.gointernal/pipeline/prompts.gointernal/stagemodel/resolver.gointernal/stagemodel/resolver_test.gointernal/view/config.gointernal/view/config_test.go
0 PR discussion threads considered. 0 summarized; 0 resolved.
Completed in 2m 49s | gpt-5.6-terra | cr 0.10.284
| Field | Value |
|---|---|
| Model | gpt-5.6-terra |
| Reviewers | go:implementation-tests, policies:conventions, structure:repo-health |
| Engine | codex_cli · gpt-5.6-terra |
| Reviewed by | cr · rianjs-bot[bot] |
| Duration | 2m 49s wall · 4m 59s compute |
| Cost | unavailable |
| Tokens | 1.8M in / 14.5k out |
Per-workstream usage
orchestrator-selection— gpt-5.6-terra- In: 18.0k
- Out: 475
- Cache read: 11.0k
- Cache create: unavailable
- Cost: unavailable
- Duration: 11s
go:implementation-tests— gpt-5.6-terra- In: 915.1k
- Out: 5.1k
- Cache read: 825.6k
- Cache create: unavailable
- Cost: unavailable
- Duration: 1m 56s
policies:conventions— gpt-5.6-terra- In: 421.2k
- Out: 3.4k
- Cache read: 357.1k
- Cache create: unavailable
- Cost: unavailable
- Duration: 1m 05s
structure:repo-health— gpt-5.6-terra- In: 414.3k
- Out: 4.7k
- Cache read: 353.0k
- Cache create: unavailable
- Cost: unavailable
- Duration: 1m 30s
orchestrator-rollup— gpt-5.6-terra- In: 39.0k
- Out: 751
- Cache read: 28.2k
- Cache create: unavailable
- Cost: unavailable
- Duration: 15s
There was a problem hiding this comment.
Automated PR Review
Reviewed commit: 70f9200785e1
Profile: codex-rianjs-bot - Posting as: rianjs-bot[bot]
Summary
| Reviewer | Findings |
|---|---|
| go:implementation-tests | 0 |
| policies:conventions | 1 |
| structure:repo-health | 0 |
policies:conventions (1 finding)
Nits - docs/architecture.md:61
The architecture guardrail was extended to reject direct
config.ResolveMaxEffortcalls as well, but this section still documents onlyResolveModelTieras restricted and checked. Update the two guardrail sentences to name both helpers, keeping the durable model-and-effort resolution contract aligned with its enforcement.
Reviewer Coverage
go:implementation-tests— complete (constrained); inspected 16 assigned files (19 inspected across reviewers):internal/architecture/model_resolution_test.go,internal/cmd/initcmd/initcmd.go,internal/cmd/initcmd/initcmd_max_effort_test.go,internal/cmd/initcmd/initcmd_test.go,internal/config/config.go,internal/config/config_max_effort_test.go,internal/modelprefs/modelprefs.go,internal/modelprefs/modelprefs_effort_test.go,internal/pipeline/artifacts.go,internal/pipeline/pipeline.go,internal/pipeline/pipeline_test.go,internal/pipeline/prompts.go,internal/stagemodel/resolver.go,internal/stagemodel/resolver_test.go,internal/view/config.go,internal/view/config_test.go; skipped: none; constraints: Focused Go tests could not run because this read-only workbench prevents Go from creating its build-cache directories.policies:conventions— complete (constrained); inspected 7 assigned files (19 inspected across reviewers):README.md,docs/architecture.md,docs/init-config-surface.md,internal/cmd/initcmd/initcmd.go,internal/config/config.go,internal/pipeline/pipeline.go,internal/view/config.go; skipped: none; constraints: Shared CLI standards were not available as a local convenience copy; review used repo-local conventions and the supplied review context.structure:repo-health— complete (constrained); inspected 9 assigned files (19 inspected across reviewers):docs/architecture.md,internal/cmd/initcmd/initcmd.go,internal/config/config.go,internal/modelprefs/modelprefs.go,internal/pipeline/artifacts.go,internal/pipeline/pipeline.go,internal/pipeline/prompts.go,internal/stagemodel/resolver.go,internal/view/config.go; skipped: none; constraints: Review scoped to the assigned files and the updated architecture guardrail context.
Inspected files (19)
README.mddocs/architecture.mddocs/init-config-surface.mdinternal/architecture/model_resolution_test.gointernal/cmd/initcmd/initcmd.gointernal/cmd/initcmd/initcmd_max_effort_test.gointernal/cmd/initcmd/initcmd_test.gointernal/config/config.gointernal/config/config_max_effort_test.gointernal/modelprefs/modelprefs.gointernal/modelprefs/modelprefs_effort_test.gointernal/pipeline/artifacts.gointernal/pipeline/pipeline.gointernal/pipeline/pipeline_test.gointernal/pipeline/prompts.gointernal/stagemodel/resolver.gointernal/stagemodel/resolver_test.gointernal/view/config.gointernal/view/config_test.go
0 PR discussion threads considered. 0 summarized; 0 resolved.
Completed in 54s | gpt-5.6-terra | cr 0.10.284
| Field | Value |
|---|---|
| Model | gpt-5.6-terra |
| Reviewers | go:implementation-tests, policies:conventions, structure:repo-health |
| Engine | codex_cli · gpt-5.6-terra |
| Reviewed by | cr · rianjs-bot[bot] |
| Duration | 54s wall · 1m 17s compute |
| Cost | unavailable |
| Tokens | 2.7M in / 17.8k out |
Per-workstream usage
go:implementation-tests— gpt-5.6-terra- In: 1.4M
- Out: 6.6k
- Cache read: 1.2M
- Cache create: unavailable
- Cost: unavailable
- Duration: 31s
policies:conventions— gpt-5.6-terra- In: 577.9k
- Out: 4.6k
- Cache read: 497.9k
- Cache create: unavailable
- Cost: unavailable
- Duration: 18s
structure:repo-health— gpt-5.6-terra- In: 657.0k
- Out: 5.8k
- Cache read: 571.4k
- Cache create: unavailable
- Cost: unavailable
- Duration: 20s
orchestrator-rollup— gpt-5.6-terra- In: 62.2k
- Out: 813
- Cache read: 48.4k
- Cache create: unavailable
- Cost: unavailable
- Duration: 6s
Why
Agent catalogs declare an absolute
effortthat becomes the provider's reasoning-effort setting (--thinkingon the Pi RPC adapter). A deployment wanting to bound spend on an expensive tier previously had one lever: edit the shared catalog. That changes the agent author's declared intent for every consumer, including ones who want the deeper review and are happy to pay for it.cralready separates portable intent from deployment choice for models — an agent declaresmodel_tier, the profile maps tiers to concrete models and sets areviewer_model_tierbaseline. Effort had no equivalent layer.What
llm.max_effort, a per-tier ceiling that sits next tomodel_map:A tier absent from the map is uncapped. It is a ceiling only — an agent declaring
lowunder amediumceiling still runs atlow.The clamp lives in
stagemodel.ResolveStageModel, whichdocs/architecture.mddesignates as the single path from profile preferences to a concrete model and effort, explicitly so "token costs and profile-level tier floors can be added without touching individual review stages."Notable
A latent trap this surfaced.
resolveReviewerRuntimeConfigdiscarded the resolver's effort and passedagent.Effortstraight through. A clamp in the resolver alone would have silently missed reviewer agents — the only path that ever reaches thelargetier, and the entire point of the feature. The resolved effort is now threaded throughreviewerRuntimeResolution.Init preservation. There is no init editor for
max_effort, so hand-editingconfig.ymlis the only way in. The runtime round trip (initLLMRuntimeDraftFromConfig→exportConfig) rebuildsLLMConfigfield by field, so without this the first latercr inittouching the runtime would silently discard the ceiling — config parses fine, cap just gone.identityKeynow includes the map too, so two runtimes differing only by ceiling no longer collide.Deliberate bypasses. Four paths ignore the ceiling because each is an explicit selection of a concrete model or effort, and all four are documented in the README:
--reviewer-effort--reviewer-modelModelOverridereturns before the clampmodel_idcr benchmark runstages.reviewers.effortis required so candidates stay comparableThe
model_idcase is the one structural hole worth knowing about: a catalog pinning bothmodel_idandeffort: highis outside a deployment's control entirely.Testing
go build ./...,golangci-lint run, andgo test ./...(2998 passed, 71 packages) all clean.New coverage: the clamp, uncapped tiers, the never-raise rule,
model_idstaying uncapped with a ceiling configured (the pre-existing exact-model tests use profiles withoutMaxEffort, so they would have passed either way), config validation,MinEffort, and the init round trip.Also exercised end to end against a real PR — a large-tier agent declaring
effort: highresolved togpt-5.6-solatmediumwhile medium-tier agents were untouched, in a live run that posted to GitHub.Follow-ups, not in this PR
--reviewer-effort/--reviewer-modelvs. the ceiling: escape hatch or absolute? Needs a contract decision, and--selection-effortis currently asymmetric (it feeds into the resolver and is capped).max_effortsupport in thecr initwizard.ModelMapRowJSON parity — text mode shows the ceiling,--jsondoes not — plus extending the architecture guardrail test to coverconfig.ResolveMaxEffort.rebaseReviewerCohort's incompatibility message should name the field that drifted; enabling a cap trips it on the first run against a saved cohort.LLMConfig.normalized()andempty()do not know aboutMaxEffort; extendingnormalized()would letResolveMaxEffortindex directly instead of scanning.