Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: add reasoning effort to model overrides - #27061

Merged
DanielleMaywood merged 2 commits into
dm/reasoning-effort-2-per-turnfrom
dm/reasoning-effort-5-model-override-effort
Jul 9, 2026
Merged

feat: add reasoning effort to model overrides#27061
DanielleMaywood merged 2 commits into
dm/reasoning-effort-2-per-turnfrom
dm/reasoning-effort-5-model-override-effort

Conversation

@DanielleMaywood

Copy link
Copy Markdown
Contributor

Summary

  • add optional reasoning_effort to deployment and personal model override API payloads
  • preserve legacy stored override formats while supporting encoded override efforts
  • apply override reasoning effort for root personal chats, title generation, and child subagent chats
  • wire reasoning effort selection into deployment and personal model override UI

Validation

  • pnpm -C site exec tsc -p .
  • go test ./coderd -run 'TestChatModelOverrides|TestUserChatPersonalModelOverrides|TestCreateChatPersonalModelOverrideRoot' -count=1
  • go test ./coderd/x/chatd -run 'TestCreateChildSubagentChat_StoresReasoningEffortOverride|TestResolveConfiguredModelOverride_AcceptsAmbientCredentialsProvider|TestResolveManualTitleModel|TestMaybeGenerateChatTitle|TestParseChatPersonalModelOverride' -count=1
  • go test ./coderd/x/chatd/... -count=1
  • make lint

Generated by Coder Agents.

@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from 26325c8 to 3685d3c Compare July 7, 2026 14:11
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from 3028ea1 to 578101a Compare July 7, 2026 14:11
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from 3685d3c to c1f9fc4 Compare July 7, 2026 23:27
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from 578101a to 19b4375 Compare July 7, 2026 23:27
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from c1f9fc4 to 2f40dd5 Compare July 7, 2026 23:44
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from 19b4375 to 8996938 Compare July 7, 2026 23:44
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from 2f40dd5 to 35efe67 Compare July 8, 2026 07:53
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from 8996938 to 7d48361 Compare July 8, 2026 07:53
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from 35efe67 to 5286cc9 Compare July 8, 2026 08:59
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from 7d48361 to a37dfd8 Compare July 8, 2026 08:59
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from 5286cc9 to cc24930 Compare July 8, 2026 09:33
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from a37dfd8 to ff41fb0 Compare July 8, 2026 09:33
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from cc24930 to fd9e20c Compare July 8, 2026 10:14
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from ff41fb0 to 59813d7 Compare July 8, 2026 10:14
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from fd9e20c to 226f860 Compare July 8, 2026 11:23
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from 59813d7 to 09834d6 Compare July 8, 2026 11:23
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from 226f860 to 1f8421d Compare July 8, 2026 12:10
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from 09834d6 to 126499d Compare July 8, 2026 12:10
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from 1f8421d to d6a7d4c Compare July 8, 2026 14:06
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from 126499d to cd1a31a Compare July 8, 2026 14:06
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from d6a7d4c to 54c3345 Compare July 8, 2026 15:10
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from cd1a31a to d8d2038 Compare July 8, 2026 15:11
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from 54c3345 to fd8c08a Compare July 8, 2026 15:18
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from d8d2038 to 63d311c Compare July 8, 2026 15:18
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from fd8c08a to 76f68fe Compare July 8, 2026 17:53
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from 63d311c to b56cf6e Compare July 8, 2026 17:53
@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Chat: Review in progress (6/6 reviewers complete) | View chat
Requested: 2026-07-09 15:27 UTC by @DanielleMaywood
Spend: $93.77 / $100.00

Review history
  • R1 (2026-07-08): 16 reviewers, 2 Note, 2 P2, 8 P3, 1 P4, COMMENT. Review
  • R2 (2026-07-09): 8 reviewers, 2 Note, 1 P0, 2 P2, 11 P3, 1 P4, REQUEST_CHANGES. Review

deep-review v0.9.0 | Round 3 | 3c16e1e..5f12b74

Last posted: Round 3, 17 findings (1 P0, 2 P2, 11 P3, 1 P4, 2 Note), APPROVE. Review

Finding inventory

Finding Inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P3 Author fixed (b4d5ac0) exp_chats.go:534 xerrors.Errorf wraps nil error on trailing-colon-with-empty-effort guard R1 Netero Yes
CRF-2 P3 Author contested; panel closed R2 (5/7 accept) title_override.go:18 parsedModelOverride / parseModelOverride duplicates parseChatModelOverride and personal_model_override.go parsing R1 Netero, Robin Yes
CRF-3 P3 Author fixed (b4d5ac0) exp_chats.go:5324 Double DB lookup for same model config in putUserChatPersonalModelOverride model-mode branch R1 Netero, Pariston Yes
CRF-4 P2 Author fixed (b4d5ac0) exp_chats.go:5075 putChatModelOverride accepts effort embedded in model_config_id, creating GET/PUT asymmetry and endpoint inconsistency R1 Hisoka P2, Knov P2, Kite Nit Yes
CRF-5 P2 Author fixed (b4d5ac0) exp_chats.go:584 "Invalid reasoning_effort." lacks valid values and Detail, inconsistent with sibling error messages in same file R1 Leorio P2, Chopper P3 Yes
CRF-6 P3 Author fixed (b4d5ac0) personal_model_override_test.go:93 ParseChatPersonalModelOverride unit test table not updated for new effort parsing branches R1 Bisky P3, Mafu-san P2, Meruem P3, Chopper P3, Pariston Nit Yes
CRF-7 P3 Author contested; panel closed R2 (4/6 accept) subagent.go:427 withResolvedReasoningEffort silently drops admin-configured effort override on JSON errors with no log R1 Hisoka P3, Mafu-san P3, Meruem P3, Chopper P3, Knov P3, Kite Note, Leorio Note Yes
CRF-8 P3 Author contested; panel closed R2 (6/6 accept) exp_chats.go:564 parseChatModelCallConfig duplicates unmarshalChatModelCallConfig in same file R1 Robin Yes
CRF-9 P3 Author fixed (b4d5ac0) exp_chats.go:597 Selectability error message doesn't include selectable values despite having the slice available R1 Leorio P3, Chopper P3 Yes
CRF-10 P3 Author fixed (b4d5ac0) CoderAgentsPageView.tsx:21 SaveModelOverride type alias hides reasoning_effort flowing through prop boundary R1 Nami Yes
CRF-11 P4 Author fixed (b4d5ac0) subagent.go:965 childSubagentChatOptions doc comment omits reasoningEffortOverride from universal field enumeration R1 Gon P2, Leorio Nit Yes
CRF-12 Note Author accepted R2 (runtime clamping consistent with existing design) exp_chats.go:1308 Personal override effort bypasses selectability check at chat creation; clamped at runtime by ResolveReasoningEffort R1 Hisoka P3 Yes
CRF-13 Note Author accepted R2 (fails closed; user sees save failure and can adjust) PersonalModelOverrideRow.tsx:281 Form value and displayed reasoning effort can diverge when stored effort is no longer selectable after admin config change R1 Knov Yes
CRF-14 Note Dropped by orchestrator (dead return values on error paths; callers correctly discard) subagent.go:218 Hard-failure error paths return unused reasoningEffort R1 Hisoka Note, Meruem Note No
CRF-15 Nit Dropped by orchestrator (minor style, two instances) exp_chats.go:5273 Identical reasoningEffort guard repeated in chat_default and deployment_default cases R1 Robin No
CRF-16 Nit Dropped by orchestrator (minor doc gap) personal_model_override.go:37 ParseChatPersonalModelOverride doc omits ReasoningEffort from malformed-state enumeration R1 Gon No
CRF-17 P0 Author fixed (5f12b74) subagent_internal_test.go:1550 Test asserts wrong type (sql.NullString vs database.NullChatReasoningEffort), deterministic failure R2 Netero, Bisky, Mafu-san P1, Meruem Yes
CRF-18 P3 Author fixed (5f12b74) subagent.go:418 withResolvedReasoningEffort has zero test coverage across all meaningful exit paths R2 Bisky P3, Mafu-san P2 Yes
CRF-19 P3 Author fixed (5f12b74) title_override.go:23 parseModelOverride has no unit test; effort-parsing path exercised by no test R2 Bisky Yes
CRF-20 P3 Author fixed (5f12b74) exp_chats.go:601 validateChatModelOverrideEffort produces empty error detail ("Must be one of .") for non-reasoning models R2 Mafuuu P3, Chopper P3 Yes
CRF-21 P4 Dropped by orchestrator (different UI contexts justify separation; styling and tooltip differences) PersonalModelOverrideRow.tsx:438 PersonalReasoningEffortRow duplicates ReasoningEffortRow slider logic R2 Robin P3, Bisky P4 No

Contested and acknowledged

CRF-2 (P3, title_override.go:18) - Triple uuid[:effort] parsing duplication

  • Finding: Three implementations of the same uuid[:effort] format parser exist across parseChatModelOverride (exp_chats.go), parseModelOverride (title_override.go), and ParseChatPersonalModelOverride (personal_model_override.go). A shared helper would eliminate divergence risk.
  • Author defense: The parsers have distinct API/storage semantics and return shapes. Sharing would add indirection without eliminating much code and would couple chatd title override parsing to the HTTP storage parser.
  • Panel closure (R2, 5/7): Five reviewers (Hisoka, Mafuuu, Pariston, Meruem, Chopper) confirmed the return types and error semantics differ enough to justify separate implementations. The coupling cost of merging outweighs the duplication risk for ~8 lines of shared logic. Two dissenting reviewers (Mafu-san, Robin) noted the effort extension increased the shared surface, but the majority found the author's defense substantive.

CRF-7 (P3, subagent.go:427) - withResolvedReasoningEffort silent error swallowing

  • Finding: withResolvedReasoningEffort silently drops admin-configured effort override when modelConfig.Options fails to unmarshal or re-marshal, with no log or doc comment.
  • Author defense: Fail-open is low impact because Options is validated on write and preserving the original config avoids breaking title generation for a corrupt row.
  • Panel closure (R2, 4/6): Four reviewers (Hisoka, Mafuuu, Pariston, Chopper) accepted that fail-open is appropriate for the title generation best-effort path. Options corruption requires bypassing write-side validation. Consequence is limited to title generation running at default effort. Two dissenting reviewers (Mafu-san P2, Meruem P3) noted zero test coverage on new code. Test coverage is tracked separately as CRF-18.

CRF-8 (P3, exp_chats.go:564) - parseChatModelCallConfig duplicates unmarshalChatModelCallConfig

  • Finding: Both functions unmarshal json.RawMessage into *ChatModelCallConfig and check for empty input. A shared core function could serve both callers.
  • Author defense: The helpers differ semantically: the validation path must surface JSON errors while the existing helper returns nil for empty/invalid/zero configs. Sharing would add branching without clarity.
  • Panel closure (R2, 6/6): Unanimous. parseChatModelCallConfig returns errors for validation; unmarshalChatModelCallConfig swallows errors for display. Different empty-input behavior (non-nil zero struct vs nil). A shared function would require error-handling branching that would be less clear than two short functions.

CRF-12 (Note, exp_chats.go:1308) - Personal override effort bypasses selectability at chat creation

  • Finding: Personal override effort passes IsValidReasoningEffort but not SelectableReasoningEfforts at chat creation. Runtime clamping via ResolveReasoningEffort handles the consequence.
  • Author defense: Runtime clamping is consistent with the direct per-turn reasoning effort path. Stored metadata can be above current max but execution resolves to the allowed value.
  • Status: Acknowledged. Runtime behavior is correct, design is consistent.

CRF-13 (Note, PersonalModelOverrideRow.tsx:281) - Form/display effort divergence after admin config change

  • Finding: When admin lowers model's max effort, the form holds stale stored value while slider shows clamped value. Save submits stale value, backend rejects.
  • Author defense: Fails closed: backend rejects stale value on save, user can adjust. No data integrity issue.
  • Status: Acknowledged. Fails closed, narrow window.

Round log

Round 1

Panel. 2 P2, 7 P3, 1 P4, 2 Note new. 3 dropped. Reviewed against 76f68fe..b56cf6e.
Reviewers: Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Gon, Leorio, Meruem, Chopper, Nami, Robin, Komugi, Ging-Go, Ging-React, Kite, Knov.
Mafuuu, Komugi, Ging-Go, Ging-React: no findings.

Round 2

Panel. CRF-1,3,4,5,6,9,10,11 addressed. CRF-2 panel closed (5/7). CRF-7 panel closed (4/6). CRF-8 panel closed (6/6). CRF-12,13 acknowledged. 1 P0, 3 P3 new. 1 dropped. Reviewed against 3c16e1e..b4d5ac0.
Reviewers: Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Meruem, Robin, Chopper.
Hisoka, Pariston: no new findings.

Round 3

Panel verification. CRF-17,18,19,20 addressed. No new findings. All open items resolved. Reviewed against 3c16e1e..5f12b74.
Reviewers: Bisky, Mafuuu, Pariston, Meruem.
Mafuuu, Pariston, Meruem: no findings. Bisky: 1 cosmetic Note (dropped).

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The feature is well-structured: reasoning effort threads cleanly through deployment overrides, personal overrides, subagent child chats, and title generation, converging at ResolveReasoningEffort for runtime clamping. Test coverage is thorough for the happy paths, with 9 new test cases covering positive flows, negative validation, round-tripping, non-model mode rejection, child chat propagation, and deployment default reflection.

Two P2s, seven P3s, one P4, two Notes.

"Someone hits this at 2 AM from the admin override settings. They get 'Invalid reasoning_effort.' THAT'S IT. No list of valid values, no guidance, nothing. Meanwhile three other endpoints in the same file hand them the exact answer."

Severity summary: 2 P2 (API contract asymmetry, error message gaps), 7 P3 (test gaps, duplication, silent errors), 1 P4, 2 Notes.


coderd/x/chatd/personal_model_override_test.go:93

P3 [CRF-6] TestParseChatPersonalModelOverride unit test table was not extended for the new model:uuid:effort parsing paths.

The function was extended (personal_model_override.go:65-79) to parse model:<uuid>:<effort> and reject model:<uuid>: (trailing colon with empty effort). The table has seven rows, none of which exercise the ReasoningEffort field. Two missing cases:

  1. model:<uuid>:high should parse with ReasoningEffort: ptr.Ref("high")
  2. model:<uuid>: (trailing colon, empty effort) should be malformed

The integration tests cover the HTTP round-trip, but the unit test is where a regression in the parsing logic would surface cheapest.

(Bisky P3, Mafu-san P2, Meruem P3, Chopper P3, Pariston Nit)

🤖

site/src/pages/AISettingsPage/CoderAgentsPage/CoderAgentsPageView.tsx:21

P3 [CRF-10] SaveModelOverride type alias declares only { readonly model_config_id: string }, but SubagentModelOverrideSettings sends { model_config_id, reasoning_effort } through this callback.

TypeScript's structural subtyping lets the wider object pass at the call site, so the runtime works. But the type is a lie: anyone refactoring this view who trusts the type and reconstructs the request from the declared shape will drop reasoning_effort silently, with no compiler complaint.

Fix: add readonly reasoning_effort?: string to the type alias.

(Nami)

🤖

coderd/x/chatd/subagent.go:964-965

P4 [CRF-11] The doc comment enumerates universal fields ("modelConfigIDOverride and planModeOverride apply to any subagent") but omits reasoningEffortOverride, which is also universal: subagent_catalog.go passes it for both general and explore subagents.

// modelConfigIDOverride, reasoningEffortOverride, and planModeOverride apply
// to any subagent.

(Gon P2, Leorio Nit)

🤖

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/exp_chats.go
rawID, rawEffort, hasEffort := strings.Cut(trimmed, ":")
modelConfigID, err := uuid.Parse(rawID)
if err != nil || (hasEffort && rawEffort == "") {
return parsedChatModelOverride{}, xerrors.Errorf("parse chat model override: %w", err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 [CRF-1] xerrors.Errorf wraps a nil error when the trailing-colon guard fires independently of uuid.Parse.

When hasEffort && rawEffort == "" is true but uuid.Parse succeeded (err == nil), xerrors.Errorf("parse chat model override: %w", nil) produces a non-nil error with message "parse chat model override: %!w(<nil>)". The error path is correct (an error is returned), but the message is nonsensical.

Fix: produce a specific error when the trailing-colon guard fires, e.g. xerrors.New("parse chat model override: trailing colon with empty reasoning effort").

(Netero)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Coder Agents generated.

Fixed. The parser now returns a specific error for the trailing colon with empty reasoning effort case instead of wrapping a nil error.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verified fixed in b4d5ac05. The parser now returns a specific error via xerrors.New for the trailing colon case.

🤖


const titleGenerationOverrideContext = "title_generation"

type parsedModelOverride struct {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 [CRF-2] parsedModelOverride / parseModelOverride duplicates parsedChatModelOverride / parseChatModelOverride from coderd/exp_chats.go:521, and a third copy exists in ParseChatPersonalModelOverride (personal_model_override.go:65-79).

All three implement the same uuid[:effort] parsing: strings.Cut(trimmed, ":") + uuid.Parse(rawID) + hasEffort && rawEffort == "" guard + effort extraction. The only differences are return types (pointer vs value UUID, error vs bool). A shared helper would eliminate the divergence risk.

(Netero, Robin)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Coder Agents generated.

Not changed intentionally. These parsers have distinct API/storage semantics and return shapes, so sharing a helper here would add indirection without eliminating much code. Keeping them separate also avoids coupling chatd title override parsing to the HTTP storage parser.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Panel closed (5/7 accept). Five reviewers confirmed the return types and error semantics differ enough to justify separate implementations. The coupling cost of merging outweighs the duplication risk for ~8 lines of shared logic.

🤖

Comment thread coderd/exp_chats.go Outdated
httpapi.Write(ctx, rw, status, *resp)
return
}
modelConfig, err := lookupEnabledChatModelConfigByID(ctx, api.Database, parsedModelConfigID)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 [CRF-3] Double DB lookup for the same model config in putUserChatPersonalModelOverride.

validateUserChatModelConfigAvailable (line 5319) internally looks up the model config via userCanUseChatModelConfig. lookupEnabledChatModelConfigByID (line 5324) then looks up the same parsedModelConfigID again to feed validateChatModelOverrideEffort. The model config could be returned from the first validation step. Compare with putChatModelOverride which uses the combined validateChatModelOverride that looks up once.

(Netero, Pariston)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Coder Agents generated.

Fixed. validateUserChatModelConfigAvailable now returns the validated model config, and putUserChatPersonalModelOverride reuses it for reasoning effort validation instead of doing a second lookup.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verified fixed in b4d5ac05. validateUserChatModelConfigAvailable now returns the model config, eliminating the second DB lookup.

🤖

Comment thread coderd/exp_chats.go Outdated
})
return
}
if parsed.reasoningEffort != nil && req.ReasoningEffort == nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 [CRF-4] putChatModelOverride accepts reasoning effort embedded in model_config_id, creating an undocumented secondary input channel and GET/PUT asymmetry.

parseChatModelOverride parses the storage format uuid:effort from req.ModelConfigID. When the field contains "uuid:high" and reasoning_effort is null, the embedded effort is silently promoted (lines 5075-5077). But the GET response always separates them (line 5042 passes nil for effort to formatChatModelOverride). This creates two asymmetries:

  1. GET/PUT mismatch: GET returns {"model_config_id": "uuid", "reasoning_effort": "high"}, but PUT also accepts {"model_config_id": "uuid:high"} without the explicit field.
  2. Endpoint mismatch: putUserChatPersonalModelOverride parses req.ModelConfigID as a plain UUID (line 5305: uuid.Parse(rawModelConfigID)). Sending {"model_config_id": "uuid:high"} to the deployment override PUT succeeds; the same payload to the personal override PUT fails.

Fix: parse req.ModelConfigID as a plain UUID in the PUT handler, taking effort only from req.ReasoningEffort. Remove lines 5075-5077. This keeps the internal storage parser separate from API input parsing.

(Hisoka P2, Knov P2, Kite Nit)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Coder Agents generated.

Fixed. putChatModelOverride now parses model_config_id as a plain UUID and rejects encoded uuid:effort API input. The stored override parser remains in place for reading the internal persisted format.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verified fixed in b4d5ac05. PUT now rejects : in model_config_id and accepts reasoning_effort as a separate field. GET/PUT symmetry restored. Test PUTRejectsEncodedModelConfigID confirms.

🤖

Comment thread coderd/exp_chats.go Outdated
}
if !chatprovider.IsValidReasoningEffort(*effort) {
return http.StatusBadRequest, &codersdk.Response{
Message: "Invalid reasoning_effort.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 [CRF-5] Error message "Invalid reasoning_effort." drops the Detail field and uses inconsistent wording compared to the same check elsewhere in this file.

Three sibling checks in the same file (lines 1312, 3241, 3409) use:

Message: "Invalid reasoning_effort value.",
Detail:  "Must be one of none, minimal, low, medium, high, xhigh, max.",

This new check uses only Message: "Invalid reasoning_effort." with no Detail. An admin hitting this at the override settings endpoint gets no guidance about valid values.

Fix: match the existing pattern.

(Leorio P2, Chopper P3)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Coder Agents generated.

Fixed. The invalid reasoning effort response now uses the same message as the sibling checks and includes the valid value detail.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verified fixed in b4d5ac05. Error messages now include valid values in Detail, consistent with sibling checks. Note: CRF-20 flags an edge case in this fix where SelectableReasoningEfforts returns nil for non-reasoning models.

🤖

}
callConfig := codersdk.ChatModelCallConfig{}
if len(modelConfig.Options) > 0 {
if err := json.Unmarshal(modelConfig.Options, &callConfig); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 [CRF-7] withResolvedReasoningEffort silently drops the admin-configured reasoning effort override when modelConfig.Options fails to unmarshal or re-marshal, with no log or error signal.

Both json.Unmarshal (line 427) and json.Marshal (line 442) errors return the original unmodified modelConfig. The function has no logger access and no doc comment explaining the fail-open contract. Its single caller (title_override.go:90) receives a modelConfig that looks valid but carries the model's base reasoning effort instead of the admin override.

The Options field is validated at write time, so corruption is unlikely. But the pattern sets a precedent where config deserialization failures silently degrade behavior. At minimum, the caller has logger access and could log when the returned config doesn't reflect the requested effort. A doc comment explaining the intentional fail-open behavior would prevent the next editor from treating the silent returns as bugs.

Additionally, this function has zero test coverage. Four exit paths (nil effort, unmarshal failure, nil resolved effort, success), none tested.

(Hisoka P3, Mafu-san P3, Meruem P3, Chopper P3, Knov P3)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Coder Agents generated.

Not changed intentionally. The fail-open behavior is low impact because Options is validated on write, and preserving the original config on unexpected marshal/unmarshal failure avoids breaking title generation for a corrupt row. I did not add the suggested broader logging/test changes in this PR5 fix pass.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Panel closed (4/6 accept). The fail-open design is appropriate for the title generation best-effort path. Options corruption requires bypassing write-side validation. Test coverage is tracked separately as CRF-18.

🤖

Comment thread coderd/exp_chats.go
}

func validateChatModelOverrideID(
func parseChatModelCallConfig(options json.RawMessage) (*codersdk.ChatModelCallConfig, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 [CRF-8] parseChatModelCallConfig duplicates unmarshalChatModelCallConfig at line 7744 of this file.

Both take json.RawMessage, unmarshal into *codersdk.ChatModelCallConfig, and check for empty input. The new function returns (ptr, error) with a zero struct on empty; the existing one returns nil on empty/error and also checks isZeroChatModelCallConfig. A shared core function returning (*ChatModelCallConfig, error) could serve both callers, with unmarshalChatModelCallConfig wrapping it to apply the zero check and swallow errors.

(Robin)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Coder Agents generated.

Not changed intentionally. The helpers differ semantically: this validation path must surface JSON errors to callers, while the existing unmarshal helper intentionally returns nil for empty, invalid, or zero configs. Sharing the core would add branching without making the call sites clearer.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Panel closed (6/6 accept). The two functions have genuinely different contracts: validation (surfaces errors) vs display (swallows errors). Sharing would require error-handling branching less clear than two short functions.

🤖

Comment thread coderd/exp_chats.go Outdated
selectableEfforts := chatprovider.SelectableReasoningEfforts(callConfig.ReasoningEffort)
if !slices.Contains(selectableEfforts, *effort) {
return http.StatusBadRequest, &codersdk.Response{
Message: "Invalid reasoning_effort: value is not selectable for the model config.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3 [CRF-9] The selectability validation error doesn't include the selectable values despite having the computed slice available on line 595.

The message explains the constraint ("value is not selectable for the model config") but doesn't name the selectable set. The admin knows their value is wrong but has to guess what the model config allows.

Fix:

return http.StatusBadRequest, &codersdk.Response{
    Message: "Invalid reasoning_effort: value is not selectable for the model config.",
    Detail:  fmt.Sprintf("Must be one of %s.", strings.Join(selectableEfforts, ", ")),
}

(Leorio P3, Chopper P3)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Coder Agents generated.

Fixed. The selectability validation response now includes a Detail listing the selectable reasoning effort values for the model config.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verified fixed in b4d5ac05. Selectability error now includes the selectable values via strings.Join. Note: CRF-20 flags an edge case where selectableEfforts is nil.

🤖

Comment thread coderd/exp_chats.go Outdated
if chatReasoningEffort == nil {
chatReasoningEffort = personalOverrideEffort
}
if chatReasoningEffort != nil && !chatprovider.IsValidReasoningEffort(*chatReasoningEffort) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note [CRF-12] When postChats uses a personal override's reasoning effort (line 1308), it validates with IsValidReasoningEffort (is this a known value?) but not SelectableReasoningEfforts (is this value within the model's current max?).

Scenario: user stores personal override with effort "high" for a model whose max is "high". Admin later lowers the model's max to "medium". User creates a chat. "high" passes validity, the chat stores LastReasoningEffort = "high", but generation_preparer.go clamps it to "medium" at runtime via ResolveReasoningEffort.

The runtime behavior is correct (clamped), and the same gap exists for the direct req.ReasoningEffort path at this line, so this is consistent with the existing design. The stored metadata diverges from what was applied, but there's no user-facing impact. Worth knowing if metadata correctness becomes important.

(Hisoka)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Coder Agents generated.

Not changed intentionally. Runtime clamping remains consistent with the direct per-turn reasoning effort path, so stored metadata can be above the current max but execution resolves to the allowed value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Acknowledged. Runtime clamping is correct and consistent with per-turn effort path.

🤖

@@ -258,6 +275,17 @@ export const PersonalModelOverrideRow: FC<PersonalModelOverrideRowProps> = ({
form.values.mode === "model"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note [CRF-13] When an admin lowers a model's max reasoning effort after a user saved their personal override, the form holds the stale stored value while the UI slider shows the clamped value from pickReasoningEffort. Saving without touching the slider submits the original "high" value, which the backend rejects as not selectable.

The backend validation is correct, so this isn't a data integrity issue. The window is narrow (requires admin config change after user override). The user would see a save failure and can adjust.

(Knov)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Coder Agents generated.

Not changed intentionally. The stale UI case fails closed: if an admin lowers max effort after a saved override, the backend rejects the stale value on save and the user can adjust it. No data integrity issue is introduced.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Acknowledged. Fails closed in a narrow window. No data integrity issue.

🤖

@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from b56cf6e to 7b9043d Compare July 9, 2026 09:15

Copy link
Copy Markdown
Contributor Author

Coder Agents generated.

Addressed the remaining top-level findings in the amended PR5 commit:

  • CRF-6: Added parser tests for model:<uuid>:<effort> and model:<uuid>:.
  • CRF-10: Updated the frontend callback type to use the generated UpdateChatModelOverrideRequest.
  • CRF-11: Updated the childSubagentChatOptions comment to include reasoningEffortOverride.

@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from 76f68fe to ead7707 Compare July 9, 2026 09:44
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from 7b9043d to e9d0d29 Compare July 9, 2026 09:45
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from ead7707 to 0b08c0a Compare July 9, 2026 10:49
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from e9d0d29 to 508e5cc Compare July 9, 2026 10:49
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from 0b08c0a to 6455ba5 Compare July 9, 2026 12:12
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from 508e5cc to 9eb79c5 Compare July 9, 2026 12:12
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from 6455ba5 to 3c16e1e Compare July 9, 2026 13:28
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from 9eb79c5 to b4d5ac0 Compare July 9, 2026 13:28
@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good progress on R1 findings: 8 of 13 addressed cleanly in one commit. The CRF-4 fix (separating model_config_id from reasoning_effort in the PUT API) is particularly well done, including a test for the rejection case. CRF-5's fix consolidated the error Detail pattern into a shared helper. CRF-6's parser test additions are thorough.

Contested findings: panel closed CRF-2 (5/7 accept duplication defense), CRF-7 (4/6 accept fail-open design for title gen), CRF-8 (6/6 accept semantic differences). Test coverage for withResolvedReasoningEffort (raised separately as CRF-18) is tracked independently from the design concern.

One P0, three P3s new this round.

"The PR description lists TestCreateChildSubagentChat_StoresReasoningEffortOverride among the validation commands. The test asserts sql.NullString against database.NullChatReasoningEffort. These are different types." The test never passed.

Severity summary: 1 P0 (deterministic test failure), 3 P3 (test gaps, fix-chain regression).

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/x/chatd/subagent_internal_test.go Outdated
Comment thread coderd/x/chatd/subagent.go
Comment thread coderd/x/chatd/title_override.go
Comment thread coderd/exp_chats.go
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from b4d5ac0 to 5f12b74 Compare July 9, 2026 15:19
@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All findings resolved across three rounds. R2 fixes (CRF-17 P0 test type mismatch, CRF-18/19 P3 test gaps, CRF-20 P3 empty error detail) verified fixed and passing. No new findings from Netero or the R3 panel (Bisky, Mafuuu, Pariston, Meruem).

The reasoning effort feature threads cleanly through deployment overrides, personal overrides, subagent child chats, and title generation. Write-time validation gates all override paths via validateChatModelOverrideEffort. Runtime clamping via ResolveReasoningEffort handles stale stored values when admin config changes. Test density rose from 34% (R1) to 44% (R3) with coverage across all new validation paths, parsers, and the withResolvedReasoningEffort helper.

Finding ledger: 21 total (2 P2, 8 P3, 2 P4, 3 Note, 3 Nit, 1 P0). 12 author-fixed, 3 panel-closed, 2 acknowledged, 4 dropped.

"These are real stones, not costume jewelry."

🤖 This review was automatically generated with Coder Agents.

@DanielleMaywood
DanielleMaywood marked this pull request as ready for review July 9, 2026 15:55

@johnstcn johnstcn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Stamped

@coderagents

coderagents Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Documentation Check

Updates Needed

  • docs/ai-coder/agents/models.md - The Model overrides section documents override layers but does not mention that a reasoning effort can now be paired with each override. Both deployment-level (admin) subagent overrides and personal user overrides now support an optional reasoning effort selection alongside the model choice. This should be reflected in the overrides description and the configurable contexts table.

Note: This PR is part of a 5-PR stack (#26974 through #27061). No PR in the stack includes narrative docs updates for reasoning effort overrides. Consider adding documentation before the stack merges to main.


Automated review via Coder Agents

@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-4-selector-slider branch from 3c16e1e to 1277de5 Compare July 9, 2026 21:01
@DanielleMaywood
DanielleMaywood force-pushed the dm/reasoning-effort-5-model-override-effort branch from 5f12b74 to a4b850a Compare July 9, 2026 21:01
Base automatically changed from dm/reasoning-effort-4-selector-slider to dm/reasoning-effort-2-per-turn July 9, 2026 22:04
@DanielleMaywood
DanielleMaywood merged commit 034b553 into dm/reasoning-effort-2-per-turn Jul 9, 2026
28 of 29 checks passed
@DanielleMaywood
DanielleMaywood deleted the dm/reasoning-effort-5-model-override-effort branch July 9, 2026 22:04
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.

2 participants