mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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.
104 lines
3.2 KiB
Go
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"},
|
|
)
|
|
})
|
|
}
|