Files
coder/coderd/x/chatd/chatprovider/model_test.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

104 lines
3.2 KiB
Go

package chatprovider_test
import (
"testing"
fantasyopenai "charm.land/fantasy/providers/openai"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/x/chatd/chatopenai"
"github.com/coder/coder/v2/coderd/x/chatd/chatprovider"
"github.com/coder/coder/v2/coderd/x/chatd/chattest"
"github.com/coder/coder/v2/codersdk"
)
func TestModelResolvesTransportFromClient(t *testing.T) {
t.Parallel()
forceResponses := true
// babbage-002 is absent from the provider SDK's known Responses model list.
client := &chattest.FakeModel{ProviderName: fantasyopenai.Name, ModelName: "babbage-002"}
tests := []struct {
name string
config *codersdk.ChatModelOpenAIConfig
want chatopenai.Transport
}{
{"NoConfig", nil, chatopenai.TransportChatCompletions},
{"ConfigWithoutOverride", &codersdk.ChatModelOpenAIConfig{}, chatopenai.TransportChatCompletions},
{"Forced", &codersdk.ChatModelOpenAIConfig{UseResponsesAPI: &forceResponses}, chatopenai.TransportResponses},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
model := chatprovider.NewModel(client, tt.config)
require.Equal(t, tt.want, model.Transport())
require.Equal(t, fantasyopenai.Name, model.Provider())
require.Equal(t, "babbage-002", model.ModelID())
require.True(t, model.Valid())
})
}
}
func TestModelZeroValueFailsClosed(t *testing.T) {
t.Parallel()
var model chatprovider.Model
require.False(t, model.Valid())
require.Equal(t, chatopenai.TransportInvalid, model.Transport())
require.Panics(t, func() {
_ = model.Transport().UsesResponses()
})
}
// A fantasy provider can return a nil client without an error; the
// constructor must yield the invalid zero value so newLanguageModel reports
// it instead of panicking on the nil dereference.
func TestNewModelNilClientIsInvalid(t *testing.T) {
t.Parallel()
model := chatprovider.NewModel(nil, nil)
require.False(t, model.Valid())
require.Equal(t, chatopenai.TransportInvalid, model.Transport())
}
func TestModelWithLanguageModelPreservesTransport(t *testing.T) {
t.Parallel()
forceResponses := true
model := chatprovider.NewModel(
&chattest.FakeModel{ProviderName: fantasyopenai.Name, ModelName: "babbage-002"},
&codersdk.ChatModelOpenAIConfig{UseResponsesAPI: &forceResponses},
)
replacement := &chattest.FakeModel{ProviderName: fantasyopenai.Name, ModelName: "babbage-002"}
wrapped := model.WithLanguageModel(replacement)
require.Equal(t, model.Transport(), wrapped.Transport())
require.Same(t, replacement, wrapped.LanguageModel())
}
// The kept transport is only correct for the identity it was resolved from,
// so replacing the client may not launder an invalid wrapper into a valid one
// or swap in a client with a different identity.
func TestModelWithLanguageModelRejectsIdentityChanges(t *testing.T) {
t.Parallel()
client := &chattest.FakeModel{ProviderName: fantasyopenai.Name, ModelName: "babbage-002"}
model := chatprovider.NewModel(client, nil)
require.Panics(t, func() {
chatprovider.Model{}.WithLanguageModel(client)
})
require.Panics(t, func() {
model.WithLanguageModel(nil)
})
require.Panics(t, func() {
model.WithLanguageModel(
&chattest.FakeModel{ProviderName: fantasyopenai.Name, ModelName: "gpt-4.1"},
)
})
}