Stacked on #27703.
Provider option conversion, reasoning effort injection, and file part
acceptance each recomputed the OpenAI wire format from `(provider,
modelID, override)`. They now read it from `chatprovider.Model`, so a
decision cannot drift from the client it was built for.
`ProviderOptionsFromChatConfig` takes a `Transport`,
`ApplyReasoningEffort` takes a `Model`, and `AcceptsFilePartMediaType`
becomes a `Model` method. `UsesResponsesAPI` and `UsesResponsesOptions`
are deleted. The override extraction is unexported and reachable only
from `ModelFromConfig`, which now takes the model's
`ChatModelOpenAIConfig` directly, removing the six scattered extractions
at call sites.
That also resolves the computer-use mismatch. The computer-use model is
a hardcoded default with no config row of its own: its client was built
without an override while request preparation applied the chat model's.
Preparation now reads the computer-use model's own transport, so the two
agree without one model's client settings following a different model.
Passing the chat model's `openai_config` into the computer-use client
would have made them agree on the wrong value.
`TestModelTransportConsumersAgree` pins the invariant in one test: the
HTTP path the client actually hits, the concrete provider option struct
type, the type created by reasoning effort, and text/image file
acceptance.
> Mux prepared this PR on Mike's behalf.
Stacked on #27683.
The OpenAI wire format is decided when the client is built, then thrown
away, so downstream sites recompute it from `(provider, modelID,
override)`. Any disagreement fails silently: the SDK type-asserts the
concrete provider options struct and discards every OpenAI option, and
text attachments are dropped because Responses natively accepts only
images and PDFs.
This adds `chatprovider.Model`, which pairs a fantasy client with the
transport resolved from that client's own identity. Its fields are
unexported and only the constructor sets the transport, deriving it from
the client, so no caller can pick a transport that disagrees with the
client it wraps. `chatopenai.Transport`'s zero value is invalid and
panics when read rather than defaulting to a wire format, following the
existing precedent for construction invariants.
`Model` is threaded through construction, the resolve paths, and the
four struct fields that store a model for later request preparation.
Terminal call sites keep taking `fantasy.LanguageModel` and receive
`LanguageModel()`, which avoids a new package edge from `chatloop` and
`chatadvisor` into `chatprovider`.
No decisions move yet. The consumers still recompute the transport, and
`UsesResponsesAPI` now delegates to `TransportFor` so the two agree by
construction. #27704 makes the consumers read it from the model.
> Mux prepared this PR on Mike's behalf.
chatd hardcoded `WithUseResponsesAPI()`, so the provider SDK's static
known-model list decided whether an OpenAI model spoke the Responses API
or Chat Completions. A model absent from that list silently fell back to
Chat Completions until the fantasy fork was patched.
This exposes the SDK's `WithResponsesAPIFunc` hook as a per-model
setting, `openai_config.use_responses_api`, stored in the existing
`chat_model_configs.options` JSONB. Unset keeps the known-model list,
`true` forces Responses, `false` forces Chat Completions. There is no
migration.
It sits in a new construction-time `openai_config` section rather than
in `provider_options.openai` because it selects the API when the client
is built, while `provider_options` holds per-request parameters. That
placement is also load-bearing: a config setting only this field would
otherwise materialize an OpenAI request-options struct and turn on
provider-side response storage, since `Store` defaults to true there.
Three places independently decided the transport and would silently
disagree with the client actually built:
| Site | Effect when it disagrees |
| --- | --- |
| `ModelFromConfig` | the transport being overridden |
| `AcceptsFilePartMediaType` | text attachments dropped, since Responses
natively accepts only images and PDFs |
| `UsesResponsesOptions` | the SDK type-asserts the concrete options
struct, so every OpenAI option is discarded |
They share one predicate here, `chatopenai.UsesResponsesAPI`, with the
override threaded to each. The rest of the stack removes that threading
by resolving the transport once and carrying it. Compaction overrides
and the quickgen debug model built clients without `ConfigOptions`, so
they now pass it and pick up both this setting and the existing
Anthropic beta headers.
The toggle also makes transport-conditional option handling
admin-switchable, so two hardening changes ride along.
`ServiceTierFromChat` now maps every tier the codersdk enum advertises
(`auto`, `default`, `flex`, `scale`, `priority`); it previously returned
nil for `default` and `scale`, so flipping a model to Responses silently
dropped a configured `service_tier` that the API accepts (fantasy
forwards the value unchanged). And a new
`TestProviderOptionsTransportParity` pins, per `provider_options.openai`
field, which transport honors it, against a table in ARCHITECTURE.md, so
a field honored on one transport and silently ignored on the other fails
the test unless recorded as intentional.
Review rounds also caught two lifecycle gaps around the new field.
`isZeroChatModelCallConfig` now inspects `OpenAIConfig`, so a stored
options blob whose only setting is this toggle survives into GET/list
responses instead of reading as `model_config: null`;
`TestIsZeroChatModelCallConfigCoversEveryField` sets each config field
in isolation and fails if any field is invisible to the zero check. And
the model editor's update path sends an explicit empty `model_config`
when an edit clears the last field, since an omitted property preserves
the stored options server-side; covered by the
`EditClearingLastOptionSendsEmptyConfig` story.
Azure keeps following the known-model list, because the Azure provider
exposes no equivalent hook. The model editor renders Azure with the
OpenAI option schema, so instead of shipping a visible but inert
control, the option schema generator gains a `providers` struct tag that
it emits as `visible_for_providers`. Gating uses the raw provider type
rather than the alias table, so the control appears only for
openai-typed providers. No hand-written frontend field: the editor
renders it from the generated schema.
Closes
https://linear.app/codercom/issue/CODAGT-874/add-completionsresponses-api-toggle-in-model-editor
> Mux prepared this PR on Mike's behalf.