Commit Graph
10 Commits
Author SHA1 Message Date
Michael Suchacz 9168dbf4d3 refactor(coderd/x/chatd): unify LLM model-call construction behind a resolver (#28145)
Centralizes every LLM client build in chatd behind one resolver and
makes all model calls behave the same, instead of preserving per-flow
quirks.

## Why

`chatd` built LLM clients in five places (standard turn, compaction
override, quickgen, title override, advisor), each repeating config
parsing, route resolution, client construction, and debug wrapping with
slightly different behavior. An earlier iteration of this PR centralized
construction but preserved every historical quirk behind
purpose-specific spec constructors and policy flags. Review concluded
the quirks themselves were the problem.

## What changed

- `Server.resolveModelCall` (`modelcall.go`) is the single pipeline from
a `modelCallSpec` to a ready client plus call metadata. Callers build
spec literals inline; the only spec fields are data: model source (chat
config, explicit config row, or fixed provider/model pair for computer
use), requested reasoning effort, chatd-scoped routing, and build
options.
- Uniform behavior for every call:
- Provider options are always derived. Whole-chat summaries, turn status
labels, and chat-model compaction summaries previously sent none; they
now follow the config like everything else.
- `MaxOutputTokens` always defaults to 32k when the config leaves it
unset. The compaction summary call nulls the cap after copying the
template because it is non-streaming and the Anthropic SDK rejects
non-streaming requests whose cap implies a completion over ten minutes.
- Debug recording always wraps the client when chat debug is enabled,
labeled with the resolved provider/model. The debug-rebuild paths
(`manualTitleDebugSpec`, `quickgenDebugSpec`) are deleted; debug helpers
now only create and finalize run records, since run association flows
through the generate context, not the model.
- Clients are always constructed with the configured model string and
unmutated route, so AI Gateway validation sees the name exactly as
configured. The resolved identity feeds metadata, logs, and debug labels
only.
- `resolvedModelCall.newCall` / `newObjectCall` are the only production
constructors of `fantasy.Call` / `fantasy.ObjectCall`; object calls now
carry the config's sampling parameters.
- Net: -240 lines.

## Validation

- Full `./coderd/x/chatd/...` suite green (PostgreSQL-backed).
- Red-green toggles verified for the three key unifications (option
derivation, token default, summary cap removal).
- ARCHITECTURE.md: prose the refactor made stale is deleted; no new
resolver documentation is added.

> Mux acted on Mike's behalf for this PR.
2026-08-20 20:54:52 +00:00
Michael Suchacz 0e16e356b0 refactor(coderd/x/chatd): read the OpenAI transport from the model (#27704)
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.
2026-08-04 08:00:19 +00:00
Michael Suchacz b5c9e8e471 refactor(coderd/x/chatd): carry the resolved OpenAI transport on a model wrapper (#27703)
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.
2026-08-04 07:46:32 +00:00
Michael Suchacz c6cee10e8b feat: add per-model OpenAI Responses API toggle (#27683)
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.
2026-08-04 09:30:27 +02:00
Michael Suchacz 997b5d0843 feat: add synthetic gateway keys (#27170)
> Mux is working on behalf of Mike.

## Summary

Add a per-user synthetic API key for chatd AI Gateway attribution. Chatd
resolves the key from the chat owner, extends it before expiry, and
discards the generated bearer token so the key is never a usable
credential.

There is no mapping table. The key is resolved from `api_keys` by a
deterministic token name (`chatd_<owner_id>_session_token`), mirroring
the provisionerd session token model, with three deltas that chatd
needs:

- **Login type guard**: token names are unvalidated user input, so a
user can create a bearer token with the colliding name. The lookup
excludes `login_type = 'token'` rows, so chatd never picks up (or
extends) a real user token. Synthetic keys are minted with the owner's
login type, which is never `token`.
- **In-place expiry extension instead of delete-and-reinsert**: chat
generations have no stop boundary, and an in-flight generation may have
already delegated the current key ID to aibridged. Extending
`expires_at` keeps the key ID stable forever.
- **Advisory-lock mint**: the unique index on token names is partial
(`WHERE login_type = 'token'`), so nothing DB-enforces uniqueness for
synthetic keys. A per-user advisory lock serializes concurrent mints.

Keys carry a minimal scope (`api_key:read`) as defense in depth; the
delegated gateway path never evaluates scopes and the secret is
discarded at mint.

Migration 000544 removes the foreign keys from the legacy message and
queue `api_key_id` columns while chatd continues stamping them for
rolling compatibility. Stale IDs are tolerated because routing uses
`chats.owner_id`. Individual key deletion, delete-all, and password
reset remove the key without changing chat history or queue versions,
and the next lookup remints it. Suspension does not delete the key;
delegated gateway authorization rejects inactive users at request time.

This is the first PR in a three-PR rollout and must be fully deployed
before #27171.

Refs
https://linear.app/codercom/issue/CODAGT-561/maintain-synthetic-api-key-per-user-per-chat
2026-07-18 20:45:13 +02:00
Michael Suchacz 1ac106255b feat: add Anthropic 1M context window toggle for Agents model configs (#27257) 2026-07-17 13:04:13 +02:00
Cian Johnston 843754a547 refactor(coderd/x/chatd): remove dead model-routing dispatch shim (#26942)
Follow-up to #26862 ("remove direct chat routing"), which collapsed the
routing discriminated union into a single `aiGatewayModelRoute` but left
a one-path dispatch shim behind in `model_routing.go`.

Removes
`resolveModelRouteForConfig`/`resolveModelRouteForProviderType`/`newModel`
wrapper functions that did nothing but call their `*AIGateway*`
counterparts, and renames the `*AIGateway*` targets to take over those
names directly. Also collapses a redundant if/else in
`title_override.go` where both branches called the same function with
the same effective argument, and has `chatutil.NormalizedStringPointer`
delegate to the existing `coderd/util/strings.EmptyToNil` instead of
reimplementing empty-string-to-nil logic.

No behavior change.

<details>
<summary>Investigation notes / decision log</summary>

Two independent read-only investigations were run over `coderd/x/chatd`
looking for cleanup opportunities following #26862: one focused on
residue from that PR specifically, one a general over-engineering pass
on the whole package. Both independently converged on the
`model_routing.go` shim as the top finding (verified zero divergent call
sites).

Other candidates considered and explicitly deferred/rejected for this
PR:

- Renaming away the vestigial `AIGateway` prefix package-wide:
cosmetic-only, touches many call sites, skipped.
- Inlining the `chatcost` subpackage into `chatd`: unrelated to #26862,
skipped.
- Deleting the deprecated `AIGatewayRoutingEnabled` deployment flag:
confirmed dead/no-op, but intentionally kept as a back-compat shim per
#26862; removal should follow the same deprecation cadence as other
deprecated deployment options, as a separate, differently-timed change.
- Folding `chatutil` entirely into `chatprovider`/`chatopenai`:
`NormalizedStringPointer` overlapped with
`coderd/util/strings.EmptyToNil` (now reused), but `NormalizedEnumValue`
has no equivalent elsewhere in the repo and still has 2 real call sites,
so the package stays.

</details>

---
Generated by Coder Agents on behalf of @johnstcn.
2026-07-02 11:22:56 +01:00
Cian Johnston 4936ff9808 refactor: deprecate AIGatewayRoutingEnabled, remove direct chat routing (#26862)
This PR removes the now-dead direct-routing code:

- Deletes the direct routing implementation.
- Collapses the resolvedModelRoute discriminated union into aiGatewayModelRoute.
- Removes the dead providerKeys cascade.
- Deletes the preferredShortTextCandidates quickgen function.
- Simplifies the advisor override error handling.
- Deprecates the AIGatewayRoutingEnabled deployment option. It is now a no-op so as to not break existing deployments on upgrade.

Once direct routing was gone, the AI Gateway became mandatory for chat, which surfaced gaps in how the product behaves with the gateway disabled:

- Exposes ai-gateway-enabled to the frontend via embedded page metadata.
- Disables the chat composer via the existing AgentSetupNotice when the gateway is disabled, for both new and existing chats.
- Fixes nil/typed-nil chatDaemon panics on startup and shutdown when gateway is disabled.
- Fixes chat WebSocket from retrying the still-gated stream endpoint forever when the gateway is disabled.
2026-07-01 20:15:03 +01:00
Hugo Dutka 4debd23cbb fix: chatd refactor (#26270)
Implements the chatd stabilization RFC.

Combines:
- https://github.com/coder/coder/pull/25908
- https://github.com/coder/coder/pull/25923
- https://github.com/coder/coder/pull/26109
- https://github.com/coder/coder/pull/26110
- https://github.com/coder/coder/pull/26111
- https://github.com/coder/coder/pull/26112
2026-06-12 13:33:12 +02:00
Michael Suchacz 8b1705eb65 feat: route chatd provider traffic through aibridge (#25629)
## Summary

Routes chatd model calls backed by concrete AI Provider rows through the
in-process aibridge transport by default, with deployment options to use
direct provider routing when AI Gateway is disabled or chat AI Gateway
routing is disabled.

- Splits model routing into common, direct provider, and AI Gateway
paths behind a single deployment-mode entry point.
- Builds chatd models through explicit request, route, and options data.
Active API key attribution is passed explicitly instead of being hidden
inside generic model construction.
- For AI Gateway BYOK routes, resolves the user's provider key in chatd,
forwards it through provider-specific auth headers, and sets
`X-Coder-AI-Governance-Token` to the `delegated` marker so aibridge
preserves those headers while still stripping Coder-specific metadata.
- Keeps central provider credentials and deployment fallback credentials
out of forwarded provider auth headers, so AI Gateway central policy
remains authoritative.
- Redacts delegated provider auth from default string formatting to
avoid accidental plaintext logging of user BYOK credentials.
- Covers selected chat models, advisor overrides, title and quickgen
paths, subagent overrides, computer use model selection, and an
integration-style chat turn through the aibridge transport path.
- Persists initiating API key IDs on chat and queued user messages,
including subagent child messages, and fails closed for AI
Gateway-routed model builds without an active key.
- Removes unused `api_key_id` indexes while keeping the persistence
columns and foreign keys.
- Keeps the deployment option available through config and env parsing,
but hides it from CLI help and generated docs.
- Stabilizes the subagent poll fallback test so background CreateChat
processing cannot win the state transition under slower CI environments.

## Tests

- `go test ./coderd/x/chatd -run
'TestAIGatewayProviderAuthForUser|TestAIGatewayProviderAuthRedactsFormatting|TestResolveModelRouteForConfigAIGatewayProviderAuth|TestAIGatewayModelForwardsProviderAuth|TestProcessChat_AIGatewayRoutingUsesDelegatedAPIKey|TestAwaitSubagentCompletion'
-count=1`
- `go test ./coderd/aibridged -run
'TestServeHTTP_DelegatedAPIKey|TestServeHTTP_StripCoderToken' -count=1`
- `git diff --check HEAD~1..HEAD`
- `make lint`

> Mux working on behalf of Mike.
2026-05-26 19:31:52 +00:00