Files
coder/coderd/x/chatd/model_routing.go
T
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

76 lines
1.8 KiB
Go

package chatd
import (
"context"
"encoding/json"
"net/http"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/x/chatd/chatprovider"
"github.com/coder/coder/v2/codersdk"
)
type modelClientRequest struct {
Chat database.Chat
ModelName string
UserAgent string
ExtraHeaders map[string]string
// ConfigOptions holds the model config row's Options JSONB; empty for
// paths without a config row.
ConfigOptions json.RawMessage
}
type modelBuildOptions struct {
ActiveAPIKeyID string
RecordHTTP bool
}
func (p *Server) enabledAIProviderByID(ctx context.Context, providerID uuid.UUID) (database.AIProvider, error) {
provider, err := p.db.GetAIProviderByID(ctx, providerID)
if err != nil {
return database.AIProvider{}, xerrors.Errorf("get AI provider: %w", err)
}
if !provider.Enabled {
return database.AIProvider{}, xerrors.Errorf("AI provider %s is disabled", provider.ID)
}
return provider, nil
}
func newLanguageModel(
providerHint string,
modelName string,
providerKeys chatprovider.ProviderAPIKeys,
userAgent string,
extraHeaders map[string]string,
httpClient *http.Client,
openAIConfig *codersdk.ChatModelOpenAIConfig,
) (chatprovider.Model, error) {
model, err := chatprovider.ModelFromConfig(
providerHint,
modelName,
providerKeys,
userAgent,
extraHeaders,
httpClient,
openAIConfig,
)
if err != nil {
return chatprovider.Model{}, err
}
if !model.Valid() {
provider, resolvedModel, resolveErr := chatprovider.ResolveModelWithProviderHint(modelName, providerHint)
if resolveErr != nil {
return chatprovider.Model{}, resolveErr
}
return chatprovider.Model{}, xerrors.Errorf(
"create model for %s/%s returned nil",
provider,
resolvedModel,
)
}
return model, nil
}