Files
coder/scripts
Michael Suchacz 2bf0f567d8 feat: add OpenAI Pro reasoning mode to chat model configs (#29584)
Adds `provider_options.openai.reasoning_mode` (`pro`) to chat model
configs so admins can turn on OpenAI's Pro reasoning mode for a model,
alongside the other OpenAI request options on the model form such as
`service_tier`. The field is offered for any model on an OpenAI
provider; the field description says Pro is supported from the GPT-5.6
Sol generation and that requests fail when Pro is set on a model that
does not support it. There is deliberately no supported-model list
anywhere: a newly released model that supports Pro works with no code
change, and OpenAI rejects the request when the model does not.
Validation rejects only values other than `pro`, non-OpenAI providers,
and configs that explicitly force plain sampling
(`reasoning_model=false`) or Chat Completions
(`use_responses_api=false`), including partial updates that move a
configured model onto such a provider or transport.

A configured mode implies the Responses API and reasoning-model
treatment when `openai_config` leaves those unset
(`chatprovider.effectiveOpenAIConfig`), so an alias the pinned SDK has
never seen, such as `gpt-daybreak-blue-latest`, still gets `effort` and
`summary` and drops `temperature`. The same effective config feeds the
client options and the recorded transport, so the two cannot disagree.

The pinned fantasy OpenAI provider options do not carry
`reasoning.mode`, so the mode is applied as a client-scoped `openai-go`
request option when the model client is built rather than per call. The
SDK merges pre-serialization `WithJSONSet` overlays by top-level key, so
the key is escaped (`reasoning\.mode`) to add `mode` to the existing
`reasoning` object instead of replacing `effort` and `summary`.
`TestResolveModelCallReasoningModeWire` pins the resulting request body
for streaming and non-streaming calls and checks that the fixed
computer-use model does not inherit the mode; AI Gateway forwarding of
the `reasoning` object is covered in `aibridge`. Because the option is
bound to the model client, every call made through a Pro model config
carries it, including title generation and quick generation when those
fall back to the chat's model; deployments that want cheaper auxiliary
calls should point the title generation override at another model.

Frontend: the field renders through the existing schema-driven provider
options with no hand-written field logic. Two generic changes:
`visible_for_providers` is now honored for provider fields the way it
already was for general fields (render and payload), so the
OpenAI-scoped select does not appear or serialize for Azure, which
shares the OpenAI field group; and the add/update model save toasts now
carry the server's error detail, so a rejected combination such as Pro
with Use Responses API at Off tells the admin which field to change.

Docs: `docs/ai-coder/agents/models.md` gains a Reasoning Mode row in the
OpenAI provider-specific options table (from the documentation check
comment
https://github.com/coder/coder/pull/29584#issuecomment-5752457808), and
its Azure note now says Azure OpenAI lacks that one option.

Not changed: `coderd/x/chatd/ARCHITECTURE.md`. Its OpenAI transport
selection table needs a `reasoning_mode` row (client-applied, neither
transport) to match `TestProviderOptionsTransportParity`; left for the
human author.

<details>
<summary>Delivery record</summary>

- Local validation on every pushed head: targeted Go tests (chatopenai,
chatprovider, chatd wire test, aibridge integration test, coderd DB
tests for persistence, invalid input, and partial updates), red/green on
the wire test (injection removed, and unescaped key), Vitest
(`modelConfigFormLogic`, `ModelForm`), Storybook tests for
`ModelFormProviderConfig`, biome, tsc, oxlint, golangci-lint on touched
packages, pre-commit hook.
- Codex round 1 on `e7dbaefeafc`: 3 findings (Storybook assertions,
ARCHITECTURE.md TODO, `gpt-daybreak-blue-latest` alias). Disposition:
assertions moved to Vitest and alias accepted in `8fccf111111`;
ARCHITECTURE.md left untouched by author instruction. Threads replied to
and resolved.
- Codex round 2 on `8fccf111111`: 1 finding (fixture naming, FE9),
renamed in `23449e92317`. Round 3 on `23449e92317`: 1 finding (fixture
location, FE9), moved to `testHelpers/chatModels.ts` in `c3bd5aa370f`.
Round 4 on `c3bd5aa370f`: "Didn't find any major issues"
(https://github.com/coder/coder/pull/29584#issuecomment-5735119236). All
threads resolved.
- Remote UAT round 1 (chat
https://dogfood.cdr.dev/agents/f5ed1d16-be1d-4d39-952b-126a6a829ef1)
exercised `e7dbaefeafc`: endorsed PASS. Real `gpt-5.6-sol` turns showed
`"mode": "pro"` / `"standard"` / no key in the Debug tab with effort and
summary intact; 14 POST and 6 PATCH rejections captured; UI select
visibility, persistence, reload, and clearing verified. Low,
non-blocking notes: the `gpt-5.6` alias revert in the model identifier
field is pre-existing; Pro is dropped silently when a model or transport
change makes it unsupported; Pro applies to auxiliary calls (see above).
- Remote UAT round 2 on `c3bd5aa370f` (same chat, alias and core
re-verify): core PASS, alias FAIL. With `gpt-daybreak-blue-latest` the
request carried `{"mode": "pro"}` without the configured effort and
summary and kept sending `temperature` (provider 400 then retry on quick
generation and title calls), because the SDK does not classify the alias
as a reasoning model. Fixed in `d4c338de767`: a configured mode now
implies a reasoning model unless `openai_config.reasoning_model` is set,
and a mode on a config that forces plain sampling is rejected. Alias
UI/API behavior, rejections, and known-model Pro/Default turns all
passed on `c3bd5aa370f`.
- Codex round 5 on `d4c338de767`: "Didn't find any major issues"
(https://github.com/coder/coder/pull/29584#issuecomment-5736190356).
Zero unresolved threads. CI green on `d4c338de767`.
- Remote UAT round 3 on `d4c338de767` (same chat): endorsed PASS. Alias
runs are single-attempt 200s with `{"effort", "summary", "mode": "pro"}`
and no `temperature` (quick generation and title calls no longer retry);
`reasoning_model=false` plus a mode is rejected by POST and PATCH with
the stored config unchanged; the Reasoning Model override at Off hides
the select and drops the mode on save; `gpt-5.6-sol` Pro turn unchanged.
Remaining low notes, none introduced here: rejection detail for a
non-OpenAI provider names the model rather than the provider; Pro is
dropped silently when an override makes it unsupported; `PATCH
model_config` replaces the whole object (pre-existing on main).

- coder-frontend-review on `d4c338de767` (8 role reviewers, 3 passes):
13 failing checkpoints, chiefly a TS mirror of the Go model regex, the
`json_name === "reasoning_mode"` gate duplicated in the render filter
and payload builder, a defaulted `model` parameter, and comment and type
nits. Fixed in `554c90d5fd9`: the pattern and provider scope are
generated onto the field schema, one predicate is shared by both call
sites, `buildModelConfigFromForm(provider, model, form)` requires the
model, and the ModelForm test also asserts the select disappears for an
unsupported model. Red/green: stripping `supported_models` or
`visible_for_providers` from the generated schema fails 21 and 1 Vitest
cases respectively. Declined with rationale: RO4 (move the fixture to
`ModelsPage/testFixtures.ts`; FE9 in `FRONTEND_PATTERNS.md` names
`testHelpers/chatModels.ts`, and Codex round 3 required it) and a DF5
nit to gate on the committed identifier and warn when a saved mode is
dropped (behavior change, separate follow-up). Final pass: all
checkpoints PASS or N/A.
- Remote UAT round 4 on `554c90d5fd9` (same chat, focused regression
after the frontend-review refactor): endorsed PASS. All six claims held
(Pro round trip with effort and tier intact; unsupported identifiers
hide the select and a save drops the stale mode while keeping the other
options; Azure never shows or saves it; the alias shows it only with the
Responses override On; Reasoning Model Off hides it and drops it; three
real `gpt-5.6-sol` Pro turns were single-attempt 200s with
`{"effort","summary","mode":"pro"}` and no `temperature`). Probes aimed
at the refactor also passed: `gpt-5X6-sol` is rejected (dot escaping
survived the JSON round trip), whitespace-padded identifiers still
match, the other 11 OpenAI fields and the Anthropic section still
render. Remaining low notes are carried over from round 3 plus one
naming note ("Reasoning Mode" versus "Reasoning Model"). The final head
`abb58df77d8` differs from `554c90d5fd9` only in `ModelForm.test.tsx`,
so it ships the same production code.
- Codex round 6 on `554c90d5fd9`: 1 finding
(https://github.com/coder/coder/pull/29584#pullrequestreview-5260492465):
the two `toBeInTheDocument` assertions added in the frontend-review
round violate FE1. Confirmed against `site/AGENTS.md`; removed in
`abb58df77d8` (test-only, 7 deletions; the payload assertion keeps the
behavior coverage and the `ProviderConfigOpenAI` story captures the
hidden state). Thread replied to and resolved.
- Codex round 7 on `abb58df77d8`: "Didn't find any major issues"
(https://github.com/coder/coder/pull/29584#issuecomment-5749626071).
Zero unresolved threads. CI green on `abb58df77d8` (23 success, 8
skipped, `required` pass).

- Design change on `e99152e5c57` (owner direction: no model gating;
"none or pro" for any model; tooltip states the GPT-5.6 Sol constraint
and that requests fail on unsupported models). Removed
`codersdk.ChatModelReasoningModeModels`, the generated
`supported_models`, `isFieldApplicableToModel`, the
`gpt-daybreak-blue-latest` special case, and the `standard` enum value;
`ValidateReasoningMode(provider, config)` no longer takes the model;
`effectiveOpenAIConfig` makes a mode imply Responses and reasoning-model
treatment. PR footprint went from +1130/-22 to +769/-18. Local
validation: `go test` for chatopenai, chatprovider, chatd wire test, and
the coderd API tests (persistence now covers `gpt-5.6-sol`, `gpt-4o`,
`gpt-daybreak-blue-latest`, `gpt-7-nova`; invalid input now rejects
`standard`), golangci-lint on touched packages, Vitest (187 tests across
`modules/aiModels` and `ModelsPage`), biome, tsc, pre-commit hook.
- coder-frontend-review on `e99152e5c57` (8 roles, 1 pass plus a TS and
CD confirmation pass): 9 failing checkpoints (SD4, CD1, CD3, CD4, TS3,
TS7 x2, RO3, RO5, TN4, DF5). Fixed in `e06bfbe4299`: the ModelForm test
edits `mockGPT5` so its provider matches the selected provider state,
types the update mock like the sibling story, and drops an unused route;
the story spreads `mockGPT5` with a saved mode instead of a dedicated
shared fixture (`MockGPT56Pro` removed, `testHelpers/chatModels.ts` back
to base); a `ProviderConfigAzure` story captures the panel without
`reasoning_mode`; comment nits. Left as a nit with justification: RO3
(the one-line `!hidden && isFieldVisibleForProvider` predicate now
appears in both `getVisibleProviderFields` and
`getVisibleGeneralFields`; hoisting a helper for two call sites is not
worth it). Confirmation pass: TS all PASS; CD flagged one wording nit,
fixed before commit.
- CI green on `e06bfbe4299` (23 success, 8 skipped, `required` pass).
- Codex round 8 on `e06bfbe4299`: 1 P2 finding
(https://github.com/coder/coder/pull/29584#discussion_r4057659673): the
form still offers Pro with Use Responses API or Reasoning Model at Off,
a combination the server rejects, and the failure was not actionable.
Disposition: the select stays visible by owner direction (no client-side
gating of this field beyond the provider scope; an explicit rejection is
preferred over the earlier silent drop), and `e2b43c3aee7` makes the
add/update save toasts show the server detail so the admin sees which
field to change. Thread replied to and resolved; zero unresolved
threads.
- Remote UAT round 5 on `e06bfbe4299` (same chat): endorsed PASS with
two UI issues to land before release. All seven probes passed: the
select offers only Default and Pro for `gpt-5.6-sol` and `gpt-4o` with
the tooltip text verbatim; Pro saves on `gpt-4o` and the real turn fails
visibly with the provider's "reasoning.mode is not supported with this
model" after a single 400 attempt, and works again once Pro is cleared;
`gpt-daybreak-blue-latest` with Pro and no overrides is a single 200 to
`/v1/responses` with `{effort, summary, mode: "pro"}` and no
`temperature`; the `gpt-5.6-sol` Pro turn is unchanged; POST and PATCH
reject `standard`, `PRO`, empty, Responses Off, Reasoning Model Off, and
Azure, openai-compat, and Anthropic providers with the stored config
unchanged, while a model-only PATCH to `gpt-4o` now succeeds; the Azure
form shows no Reasoning Mode; the alias with an explicit Responses
override still works. Reservations: (1) medium, the UI conflict path
only showed "Invalid model config." without the detail (fixed in
`e2b43c3aee7`, not yet UAT-exercised); (2) low-medium, a dev-DB row
still holding `standard` from round 2 renders an empty red select whose
message is the whole field description plus "has an invalid value." and
blocks saving unrelated fields (pre-existing generic behavior of
`yupTestForField` using the description as the label; no released data
can hold `standard`, so no migration); (3) low, no hint next to older
identifiers (documented design); (4) low, a chat on such a stored row
fails with a generic headline (pre-existing).
- Final head `e2b43c3aee7` differs from `e06bfbe4299` only in the two
save-toast handlers (`AddModelPage.tsx`, `UpdateModelPage.tsx`, 8
insertions). CI is green on `e2b43c3aee7` (skipped=8, success=23,
`required` success). Codex has not run on `e2b43c3aee7`.
- Remote UAT round 6 on `e2b43c3aee7` (same chat,
https://dogfood.cdr.dev/agents/f5ed1d16-be1d-4d39-952b-126a6a829ef1,
settled 2026-09-20T19:02Z): endorsed PASS on the exact final head. Build
proven by `/api/v2/buildinfo` (`v2.37.2-devel+e2b43c3aee`,
`external_url` at the full SHA) and the UI badge. The round 5 toast
issue is resolved: Add and Update saves with Pro plus Use Responses API
Off or Reasoning Model Off show "Invalid model config." with the server
detail as the description, one 400 each, nothing stored. Compact
regression of every round 5 probe held (dropdown and tooltip; Pro on
`gpt-4o` fails visibly with the provider's `reasoning.mode is not
supported with this model` and works after clearing; alias with and
without the Responses override and `gpt-5.6-sol` all send `{effort,
summary, mode: "pro"}` with no `temperature` in a single 200; POST and
PATCH rejection matrices with stored config unchanged; Azure form
without the field). Legacy `standard` row: fails closed on chat, and
re-picking Pro or Default saves. Remaining notes, none blocking: the
malformed validation copy for an invalid stored enum (pre-existing
`yupTestForField` label choice, dev-DB rows only), Pro offered for
models that cannot accept it (documented design), and a pre-existing
`quickgen` status quirk on `gpt-4o` unrelated to this field. Secret scan
clean.
- Final head `4f3c6106dfe` adds one docs-only commit on top of
`e2b43c3aee7` (`docs/ai-coder/agents/models.md`, 9 insertions, 8
deletions, no code). Remote UAT round 6 exercised `e2b43c3aee7`; the
docs commit is not UAT-relevant. Validated with
`markdown-table-formatter`, `markdownlint-cli2`, and the pre-commit hook
(`fmt/markdown`, `lint/markdown`, `lint/emdash`, `lint/typos`). CI is
green on `4f3c6106dfe` (success=24, skipped=7, `required` success) and
the documentation check re-run marked its item addressed
(https://github.com/coder/coder/pull/29584#issuecomment-5752457808).
- Review tally for this PR: Codex 8 completed rounds;
coder-frontend-review 34 role passes (24 on `d4c338de767`, 10 on
`e99152e5c57`); remote UAT 5 rounds; no human review yet. This is far
past the six-review guardrail, so no further Codex or agent review has
been requested. Whether to spend another Codex pass on `e2b43c3aee7` is
an owner decision; human review is still required before merge.

</details>

> Xum acted on behalf of @ibetitsmike.
2026-09-21 15:58:18 +02:00
..