mirror of
https://github.com/coder/coder.git
synced 2026-11-02 05:23:10 +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.
61 lines
2.2 KiB
Go
61 lines
2.2 KiB
Go
package chatprovider
|
|
|
|
import (
|
|
"charm.land/fantasy"
|
|
|
|
"github.com/coder/coder/v2/coderd/x/chatd/chatopenai"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
)
|
|
|
|
// Model pairs a language model client with the facts resolved when it was
|
|
// built. Its fields are unexported and nothing sets transport except NewModel,
|
|
// which derives it from the client, so no caller can choose a transport that
|
|
// disagrees with the client it wraps.
|
|
type Model struct {
|
|
lm fantasy.LanguageModel
|
|
transport chatopenai.Transport
|
|
}
|
|
|
|
// NewModel pairs an already-built client with the transport resolved from that
|
|
// client's own identity. Callers must pass the config the client was built
|
|
// with, the one degree of freedom Model cannot police. ModelFromConfig is the
|
|
// only production caller.
|
|
func NewModel(lm fantasy.LanguageModel, openAIConfig *codersdk.ChatModelOpenAIConfig) Model {
|
|
if lm == nil {
|
|
// The invalid zero value lets callers report a nil client as an
|
|
// error instead of dereferencing it here.
|
|
return Model{}
|
|
}
|
|
return Model{
|
|
lm: lm,
|
|
transport: chatopenai.TransportFor(lm.Provider(), lm.Model(), openAIResponsesAPIOverride(openAIConfig)),
|
|
}
|
|
}
|
|
|
|
func (m Model) LanguageModel() fantasy.LanguageModel { return m.lm }
|
|
|
|
func (m Model) Provider() string { return m.lm.Provider() }
|
|
|
|
func (m Model) ModelID() string { return m.lm.Model() }
|
|
|
|
func (m Model) Transport() chatopenai.Transport { return m.transport }
|
|
|
|
// Valid reports whether m wraps a client rather than being the zero value.
|
|
func (m Model) Valid() bool { return m.lm != nil }
|
|
|
|
// WithLanguageModel replaces the wrapped client and keeps the resolved
|
|
// transport, because decorators such as debug recording do not change what the
|
|
// client speaks. It panics on an invalid receiver or a replacement that
|
|
// reports a different identity, because both would pair the kept transport
|
|
// with a client it was not resolved from.
|
|
func (m Model) WithLanguageModel(lm fantasy.LanguageModel) Model {
|
|
if !m.Valid() {
|
|
panic("chatprovider: WithLanguageModel on an invalid Model")
|
|
}
|
|
if lm == nil || lm.Provider() != m.lm.Provider() || lm.Model() != m.lm.Model() {
|
|
panic("chatprovider: WithLanguageModel replacement changes the model identity")
|
|
}
|
|
m.lm = lm
|
|
return m
|
|
}
|