feat: add configurable reasoning effort to Coder agents - #26974
Conversation
Docs preview📖 View docs preview for |
4968c7e to
15d4c72
Compare
|
/coder-agents-review |
|
Chat: Review posted | View chat Review historydeep-review v0.9.0 | Round 4 | Last posted: Round 4, 21 findings (3 P2, 8 P3, 1 P4, 6 Nit, 3 Note), APPROVE. Review Finding inventoryFindings
Contested and acknowledgedCRF-4 (Note, site/src/pages/AgentsPage/utils/reasoningEffort.ts:2) - unused exports
CRF-5 (P2, coderd/exp_chats.go:7577) - global-scale validation gap
CRF-8 (P4, coderd/x/chatd/chatprovider/reasoningeffort.go:100) - Google provider missing
CRF-12 (P3, site/src/pages/AgentsPage/utils/reasoningEffort.ts:2) - "Xhigh" label
Law analysisEffective LOC: 1254 additions. Head SHA: 15d4c72. Verdict: Don't split. Enforcement: Advisory. Round logRound 1Netero + Law. 1 P2, 2 P3, 1 Note. Reviewed against 07f4c9f..15d4c72. Law verdict: Don't split (Advisory). Netero P2 gates panel. Round 2Churn guard: PROCEED. CRF-1, CRF-2, CRF-3 fixed in d39150f. CRF-4 acknowledged. Panel: 19 reviewers. Netero: no new findings. 1 P2, 5 P3, 1 P4, 3 Nit, 1 Note new. Reviewed against 07f4c9f..d39150f. Round 3Churn guard: PROCEED. CRF-6, CRF-7, CRF-9, CRF-10, CRF-11, CRF-13, CRF-14, CRF-15 fixed in 355f182. CRF-5, CRF-8, CRF-12 contested. Panel: 9 reviewers (Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Leorio, Meruem, Knov, Kite). CRF-5 closed (4/5 accept), CRF-8 closed (5/5), CRF-12 closed (3/5). 1 P2, 1 P3, 2 Nit, 1 Note new. Reviewed against 07f4c9f..3d36549. Round 4Churn guard: PROCEED. CRF-16, CRF-17, CRF-18, CRF-19, CRF-20 fixed in f677efc. PR description updated. Panel: 4 reviewers (Bisky, Mafuuu, Pariston, Meruem). Netero: no findings. 1 Nit new (CRF-21). Reviewed against 07f4c9f..f677efc. About deep-reviewCRF = Coder Review Finding (P0-P4, Nit, Note)
|
There was a problem hiding this comment.
First-pass review (Netero). This is a mechanical-and-coverage scan; the full review panel has not yet reviewed this PR.
The reasoning effort refactor is well structured: clean separation of the new resolution logic into its own module, thorough migration with fixtures, and good test coverage (~50% density). The concern decomposition tracks a single feature across the stack with tight coupling, so Law says no split.
Severity count: 1 P2, 2 P3, 1 Note.
The P2 is a documentation/implementation mismatch worth fixing before the panel reviews. The panel will review after these findings are addressed.
"The comment makes a factual claim about behavior that does not exist. Either implement mirroring or fix the comment." (Netero)
🤖 This review was automatically generated with Coder Agents.
228a7db to
d39150f
Compare
|
/coder-agents-review |
There was a problem hiding this comment.
Panel review (round 2, 19 reviewers). R1 findings (CRF-1 through CRF-3) all verified fixed in d39150f. CRF-4 acknowledged as follow-up scaffolding.
The reasoning effort refactor is well-structured. The resolution pipeline (validate at API boundary, resolve with clamping at generation time, provider-specific injection at the last mile) is cleanly layered with single responsibilities. The migration is solid: nullable columns avoid rewrites, lower(trim(...)) normalization with NULLIF and IN-clause whitelisting handles edge cases, and the test fixture seeds real-world variations. Test density is 53.4%, covering validation, resolution, and the full generation chain.
Severity count: 1 P2, 5 P3, 1 P4, 3 Nit, 1 Note.
The P2 is a widened perimeter: the old per-provider validation was removed, so admins can now save global-scale effort values that individual providers reject at runtime, breaking all chats using that model. Seven reviewers flagged this independently.
"The old code had per-provider normalization... This PR removes that layer without adding a replacement." (Mafuuu)
"The config API advertises all seven global-scale values, the resolution accepts them, and the provider API rejects them. The gap between 'the API accepts it' and 'the runtime uses it' is the finding." (Razor)
🤖 This review was automatically generated with Coder Agents.
d39150f to
355f182
Compare
|
Fixed CRF-6, CRF-7, CRF-9, CRF-10, CRF-11, CRF-13, CRF-14, and CRF-15 in 355f182. Intentionally not changed:
Coder Agents generated. |
355f182 to
3d36549
Compare
|
Follow-up: also renumbered the migration to avoid the current Coder Agents generated. |
|
/coder-agents-review |
There was a problem hiding this comment.
Panel review (round 3, 9 reviewers). R2 fixes (CRF-6, 7, 9, 10, 11, 13, 14, 15) all verified correct and complete.
Contested findings resolved by panel vote:
- CRF-5 (P2, validation gap): closed 4/5. The design trades upfront strictness for flexibility. Runtime validation is the authoritative source, and per-provider sets would require manual maintenance that could drift.
- CRF-8 (P4, Google missing): closed 5/5. Not a regression; Google uses a different mechanism.
- CRF-12 (P3, "Xhigh" label): closed 3/5. Deferred to the downstream slider PR.
Severity count (new): 1 P2, 1 P3, 2 Nit, 1 Note.
The P2 is a missed call site: the advisor model setup builds provider options but never applies reasoning effort, unlike the generation and title-generation paths.
Note: the PR description still contains two inaccurate claims: (1) "values must be in the provider's runtime-supported set" (validation checks global scale only, as CRF-5 confirmed), and (2) "a single value mirrors into the other" (validation requires both, as CRF-1 established). Please update the description to match the actual behavior.
"Two of the three call sites were updated; this one was missed." (Knov)
coderd/x/chatd/chatd.go:401
P2 [CRF-16] Advisor model setup drops reasoning effort after the refactoring.
ProviderOptionsFromChatModelConfig builds provider options for the advisor model here, but ApplyReasoningEffort is never called. The generation path (generation_preparer.go:534-537) and title generation path (quickgen.go:359-360 via titleGenerationProviderOptions) were both updated to call ApplyReasoningEffort after ProviderOptionsFromChatModelConfig. This call site was missed.
The advisorCallConfig carries the new ReasoningEffort field (from the fallback config or override config's migrated options). The effort is present in the config but never injected into provider options, so the advisor model operates without its configured reasoning effort.
Fix:
providerOptions := chatprovider.ProviderOptionsFromChatModelConfig(
advisorModel,
advisorCallConfig.ProviderOptions,
)
providerOptions = chatprovider.ApplyReasoningEffort(
advisorModel,
providerOptions,
chatprovider.ResolveReasoningEffort(advisorCallConfig.ReasoningEffort),
)(Knov)
🤖
🤖 This review was automatically generated with Coder Agents.
| require.Equal(t, fantasyopenai.ReasoningEffortHigh, *providerOptions.ReasoningEffort) | ||
| }) | ||
|
|
||
| tests := []struct { |
There was a problem hiding this comment.
P3 [CRF-17] TestApplyReasoningEffort has no "Preserves" test for OpenAI, leaving the two mutation branches in applyOpenAIReasoningEffort untested.
The OpenAI apply function has a three-way type switch: *ResponsesProviderOptions (mutate existing), *ProviderOptions (mutate existing), and default (create new). Only the default branch is tested via CreatesOpenAIResponsesEntry. Every other provider has a "Preserves" test proving apply doesn't clobber existing fields. OpenAI has none. Coverage: applyOpenAIReasoningEffort at 66.7%.
In production, ApplyReasoningEffort runs after ProviderOptionsFromChatModelConfig, which builds initial options from admin config. When an admin configures both reasoning effort and another OpenAI setting, the options map already contains an OpenAI entry. That path hits lines 118-121 (mutation branches), which are untested.
Fix: add PreservesOpenAIResponsesEntry and PreservesOpenAILegacyEntry subtests, analogous to the existing preserve tests for other providers. (Bisky P3, Hisoka P3)
🤖
There was a problem hiding this comment.
Fixed CRF-17 in f677efc by adding OpenAI responses and legacy preserve coverage.
Coder Agents generated.
| @@ -0,0 +1,105 @@ | |||
| -- Chat model configs carrying legacy per-provider reasoning effort | |||
| -- values inside options. Inserted at 000535 so the 000540 data | |||
There was a problem hiding this comment.
Nit [CRF-18] Fixture comment references migration 000540 but the actual migration is 000541.
Line 2 says "the 000540 data migration" but the migration file is 000541_chat_reasoning_effort.up.sql. Stale from the renumber in this round. (Meruem, Kite)
🤖
There was a problem hiding this comment.
Fixed CRF-18 in f677efc by correcting the fixture comment to reference migration 000541.
Coder Agents generated.
| ContextLimit int64 `json:"context_limit"` | ||
| CompressionThreshold int32 `json:"compression_threshold"` | ||
| ModelConfig *ChatModelCallConfig `json:"model_config,omitempty"` | ||
| ReasoningEfforts []string `json:"reasoning_efforts,omitempty"` |
There was a problem hiding this comment.
Nit [CRF-19] ReasoningEfforts field has no doc comment.
Every other field on ChatModelConfig is self-evident from its name, but ReasoningEfforts []string is ambiguous. Is it the configured effort pair? The selectable values for UI dropdowns? The raw enum constants?
The field is computed by chatprovider.SelectableReasoningEfforts and lists the global effort values from "none" through the configured max. A one-line comment makes that clear. (Leorio)
🤖
There was a problem hiding this comment.
Fixed CRF-19 in f677efc by documenting that ReasoningEfforts contains selectable values through the configured max.
Coder Agents generated.
| ) | ||
|
|
||
| // ChatModelReasoningEffortValues is the global reasoning effort scale. | ||
| var ChatModelReasoningEffortValues = []string{ |
There was a problem hiding this comment.
Note [CRF-20] ChatModelReasoningEffortValues is an exported mutable var slice.
Any consumer can mutate the global scale (e.g., codersdk.ChatModelReasoningEffortValues[0] = "foo"), which would corrupt reasoningEffortRank, IsValidReasoningEffort, and SelectableReasoningEfforts across the entire process. Internal consumers are defensive (slices.Index is read-only, SelectableReasoningEfforts clones), and Go convention treats exported vars as read-only. Practical risk is low. Worth converting to a function or private var in a future cleanup. (Meruem)
🤖
There was a problem hiding this comment.
Fixed CRF-20 in f677efc by making the global scale private and exposing ChatModelReasoningEffortValues() as a cloned slice.
Coder Agents generated.
3d36549 to
f677efc
Compare
|
Fixed CRF-16, CRF-17, CRF-18, CRF-19, and CRF-20 in f677efc. The PR body stack and validation wording are corrected. Coder Agents generated. |
|
Fixed CRF-16 in f677efc by applying the advisor model config reasoning effort when building advisor provider options. Coder Agents generated. |
|
/coder-agents-review |
There was a problem hiding this comment.
All findings resolved across 4 rounds (21 findings total: 16 fixed, 3 closed by panel vote, 1 acknowledged, 1 minor nit below).
R4 fixes verified: CRF-16 (advisor model now applies reasoning effort, with dedicated test), CRF-17 (OpenAI preserve tests added for both Responses and legacy API shapes), CRF-18 (fixture comment corrected), CRF-19 (doc comment added), CRF-20 (exported mutable var converted to function returning clone). PR description updated to match actual behavior.
Test density: 57.8%. Netero clean across all 8 categories. The pipeline (validate at API boundary, resolve with clamping, provider-specific injection) is cleanly layered. Migration handles edge cases well. The TestChatModelReasoningEffortConfigEnumTags test mechanically prevents enum drift.
🤖 This review was automatically generated with Coder Agents.
c485ab1 to
a1bbe9e
Compare
Documentation CheckUpdates Needed
Automated review via Coder Agents |
There was a problem hiding this comment.
TODO: need to also update the coderd provider
There was a problem hiding this comment.
Coder Agents generated.
This needs a separate follow-up PR in coder/terraform-provider-coderd after the SDK bump. That provider lives in a separate repo, and model_config is currently opaque JSON there, so I left this PR scoped to coder/coder.
| ChatModelReasoningEffortNone = "none" | ||
| ChatModelReasoningEffortMinimal = "minimal" | ||
| ChatModelReasoningEffortLow = "low" | ||
| ChatModelReasoningEffortMedium = "medium" | ||
| ChatModelReasoningEffortHigh = "high" | ||
| ChatModelReasoningEffortXHigh = "xhigh" | ||
| ChatModelReasoningEffortMax = "max" |
There was a problem hiding this comment.
suggestion: should these be numeric with a String() method?
There was a problem hiding this comment.
Possibly wise, yeah. Will change
There was a problem hiding this comment.
Coder Agents generated.
I left this unchanged. A numeric enum with string JSON would require custom handling across JSON/OpenAPI/TypeScript generation and modeloptionsgen, which is out of scope for this stack. The current SDK pattern here is string-backed enums.
a1bbe9e to
a1ccef1
Compare
mafredri
left a comment
There was a problem hiding this comment.
Writing critique of this PR's title, description, and code comments, produced with the writing-critique method: each unit checked against clear-writing guides, every claim verified against the diff.
Ten findings. Inline suggestions are attached for the three code-comment findings (CF1-CF3) and can be applied directly. The title and description findings (F1-F7) are below, because they are not part of the diff and cannot carry inline suggestions.
Three findings need a maintainer fact: F2, F7, and CF1.
Title and description
F1 - title contradicts the body on the old state. The title calls the old state "per-provider"; para 1 calls it a "fixed per-model setting stored at provider-specific paths." The value was one-per-model-config keyed by provider, not shared per provider, so "per-provider" is wider than the evidence. Proposed title:
feat: replace fixed reasoning effort with a per-model default and max
F2 - Validation bullet asserts a behavior not in this diff (needs a fact). "Updates fail when the config's provider has been soft-deleted" has no supporting change anywhere in the diff; the only validation added is validateChatModelReasoningEffortConfig. If it is not introduced here, drop the sentence:
-- **Validation**: `default` and `max` must both be present when reasoning effort is configured, each value must be in the global scale `none < minimal < low < medium < high < xhigh < max`, and `default <= max`. Updates fail when the config's provider has been soft-deleted.
+- **Validation**: `default` and `max` must both be present when reasoning effort is configured, each value must be in the global scale `none < minimal < low < medium < high < xhigh < max`, and `default <= max`.If it is introduced or changed here, keep it but cite where.
F3 - "site" bullet contradicts itself and carries jargon. "compile fallout only" says nothing behavioral changed, then the same sentence reports new default <= max validation. It also uses the coined "compile fallout" / "known-model catalog" and UI jargon "selects".
-- **site**: compile fallout only; the admin form schema loses the old per-provider effort selects, known-model catalog defaults populate the new pair, and form logic validates `default <= max`. New admin UI placement and the chat slider land in follow-ups.
+- **site**: mostly mechanical changes forced by the removed fields. The admin form no longer shows the old per-provider effort dropdowns, and default values for recognized models fill the new `default` and `max` fields. The one behavior change is form validation that rejects a `default` greater than `max`. New admin UI placement and the chat slider land in follow-ups.F4 - Stack line hides a branching tree behind linear arrows. Verified: #26975 and #26976 both branch off this PR; #26977 off #26975; #27061 off #26977. The -> / (on X) notation cannot express that.
-Stack: this PR → per-turn API (#26975) and models page UI (#26976, on this PR); chat slider (#26977, on #26975) → model override reasoning effort (#27061, on #26977).
+Stack (each PR is based on the one named):
+
+- this PR (on `main`)
+- #26975 per-turn API (on this PR)
+- #26976 models page UI (on this PR)
+- #26977 chat slider (on #26975)
+- #27061 model override reasoning effort (on #26977)F5 + F7 - "behavior-preserving" is undefined-term jargon and omits its scope (F7 needs a fact). Para 2 relies on "global scale" before it is defined (it is defined only in the Validation bullet), and asserts "behavior-preserving" without qualification. The migration filters with AND legacy.effort IN ('none', ..., 'max') and so drops out-of-scale legacy values; the testdata's GPT-5.1 (Invalid Legacy Effort) = "extreme" becomes no effort, where the old path passed it straight through. Combined fix:
-This PR is behavior-preserving: generation resolves the configured default (clamped to max on the global scale), which matches the old fixed value after migration. Per-turn user selection lands in the follow-up PR.
+This PR is behavior-preserving for efforts that were already a recognized value: generation resolves the configured default, clamped to the configured max, which matches the old fixed value after migration. A stored value the new schema does not recognize (for example a manually set `"extreme"`) is dropped by the migration rather than carried over, so that config resolves to no effort. Per-turn user selection lands in the follow-up PR.If out-of-scale rows cannot exist because the old write path enforced each provider's enum, the second sentence can shrink to a one-clause scope note.
F6 - "fantasy provider options" is an undefined library name. The chatd bullet says the value is injected into the "fantasy provider options"; the ARCHITECTURE.md section this same PR adds calls them the "provider-native options". Use the term the PR's own docs use.
-- **chatd**: new `chatprovider.ResolveReasoningEffort` clamps the configured default to the configured max on the global scale before `ApplyReasoningEffort` injects it into the fantasy provider options (azure routes via openai, bedrock via anthropic).
+- **chatd**: new `chatprovider.ResolveReasoningEffort` clamps the configured default to the configured max before `ApplyReasoningEffort` injects it into the provider-native options (azure routes via openai, bedrock via anthropic).The three code-comment findings are attached inline as applicable suggestions.
🤖 This review was automatically generated with Coder Agents.
| CacheWritePricePerMillionTokens *decimal.Decimal `json:"cache_write_price_per_million_tokens,omitempty" description:"Cache write or cache creation token price in USD per 1M tokens"` | ||
| } | ||
|
|
||
| // ChatModelReasoningEffort values. |
There was a problem hiding this comment.
CF2 (minor): this header names a type ChatModelReasoningEffort that does not exist (the members are untyped string constants) and otherwise restates the names. State the non-obvious fact instead: the declaration order is the ranking used to clamp and compare.
| // ChatModelReasoningEffort values. | |
| // Reasoning effort levels, ordered low to high. The declaration order is the | |
| // ranking used to clamp and compare efforts. Keep in sync with | |
| // chatModelReasoningEffortValues below. |
🤖
There was a problem hiding this comment.
Fixed in 8f4025c. I reworded the constants comment to describe the ordering/ranking used for clamping and comparison.
Coder Agents generated.
| // The catalog carries a single editorial effort value. Write both | ||
| // reasoning_effort bounds because the API requires default and max. |
There was a problem hiding this comment.
CF3 (minor): "editorial" is undefined here; the intended meaning is the single curated value the catalog ships.
| // The catalog carries a single editorial effort value. Write both | |
| // reasoning_effort bounds because the API requires default and max. | |
| // The catalog carries a single curated effort value. Write it to both | |
| // reasoning_effort bounds because the API requires default and max. |
🤖
There was a problem hiding this comment.
Fixed in 8f4025c. I changed editorial to curated in the known-model defaults comment.
Coder Agents generated.
a1ccef1 to
8f4025c
Compare
…-model-config # Conflicts: # coderd/database/queries.sql.go # coderd/x/chatd/chatd.go # coderd/x/chatd/title_override_internal_test.go
Summary
Add configurable reasoning effort throughout Coder agents. Administrators can define a default and maximum effort for each model, users can select an effort per chat turn, and deployment or personal model overrides can request a specific effort.
Problem
Reasoning effort was stored as a fixed value in provider-specific model options. Offering the same model at different effort levels required duplicate model configs, chat turns could not select their own effort, and model overrides could not specify effort independently.
Changes
none < minimal < low < medium < high < xhigh < maxand validate each model's top-level{default, max}configuration.Migration note: Migration
000542_chat_reasoning_effortadds a database enum and nullable effort columns to chats, messages, and queued messages. It copies recognized legacy provider-specific values into the new top-level model configuration as bothdefaultandmax, while retaining the legacy keys for reversibility. Invalid legacy values are not copied, and existing chat rows remain unset so runtime fallback uses the configured model default.This PR consolidates the changes previously reviewed in #26975, #26976, #26977, and #27061.
🤖 Generated by Coder Agents on behalf of @DanielleMaywood