mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
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.
165 lines
4.9 KiB
Go
165 lines
4.9 KiB
Go
package chatd
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
"cdr.dev/slog/v3"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
"github.com/coder/coder/v2/coderd/x/chatd/chatloop"
|
|
openaicomputeruse "github.com/coder/coder/v2/coderd/x/chatd/chatopenai/computeruse"
|
|
"github.com/coder/coder/v2/coderd/x/chatd/chatprovider"
|
|
"github.com/coder/coder/v2/coderd/x/chatd/chattool"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
|
"github.com/coder/quartz"
|
|
)
|
|
|
|
// computerUseConfigContext lets internal and worker callers read
|
|
// deployment-wide chat settings when they lack an HTTP-derived actor. HTTP
|
|
// handlers always carry an actor, so the AsChatd fallback never elevates user
|
|
// contexts and this function is a no-op in that path. The setting it gates is
|
|
// global and readable by any authenticated actor, not a back-door.
|
|
func computerUseConfigContext(ctx context.Context) context.Context {
|
|
if _, ok := dbauthz.ActorFromContext(ctx); ok {
|
|
return ctx
|
|
}
|
|
//nolint:gocritic // Worker contexts may lack an actor.
|
|
return dbauthz.AsChatd(ctx)
|
|
}
|
|
|
|
func (p *Server) computerUseProviderAndModelFromConfig(
|
|
ctx context.Context,
|
|
) (provider codersdk.ChatComputerUseProvider, modelProvider, modelName string, err error) {
|
|
rawProvider, err := p.db.GetChatComputerUseProvider(
|
|
computerUseConfigContext(ctx),
|
|
)
|
|
if err != nil {
|
|
return "", "", "", xerrors.Errorf("get computer use provider: %w", err)
|
|
}
|
|
|
|
provider = chattool.DefaultComputerUseProvider(
|
|
codersdk.ChatComputerUseProvider(strings.TrimSpace(rawProvider)),
|
|
)
|
|
|
|
modelProvider, modelName, ok := chattool.DefaultComputerUseModel(provider)
|
|
if !ok {
|
|
return "", "", "", xerrors.Errorf(
|
|
"unknown computer-use provider %q configured in agents_computer_use_provider",
|
|
provider,
|
|
)
|
|
}
|
|
|
|
return provider, modelProvider, modelName, nil
|
|
}
|
|
|
|
func (p *Server) resolveComputerUseModel(
|
|
ctx context.Context,
|
|
chat database.Chat,
|
|
route aiGatewayModelRoute,
|
|
computerUseProvider codersdk.ChatComputerUseProvider,
|
|
computerUseModelProvider string,
|
|
computerUseModelName string,
|
|
modelOpts modelBuildOptions,
|
|
) (
|
|
model chatprovider.Model,
|
|
debugEnabled bool,
|
|
resolvedProvider string,
|
|
resolvedModel string,
|
|
err error,
|
|
) {
|
|
resolvedProvider, resolvedModel, err = chatprovider.ResolveModelWithProviderHint(
|
|
computerUseModelName,
|
|
computerUseModelProvider,
|
|
)
|
|
if err != nil {
|
|
return chatprovider.Model{}, false, "", "", xerrors.Errorf(
|
|
"resolve computer use model metadata for provider %q model %q: %w",
|
|
computerUseProvider,
|
|
computerUseModelName,
|
|
err,
|
|
)
|
|
}
|
|
|
|
model, debugEnabled, err = p.newDebugAwareModel(ctx, modelClientRequest{
|
|
Chat: chat,
|
|
ModelName: computerUseModelName,
|
|
UserAgent: chatprovider.UserAgent(),
|
|
ExtraHeaders: chatprovider.CoderHeaders(chat),
|
|
}, route, modelOpts)
|
|
if err != nil {
|
|
return chatprovider.Model{}, false, "", "", xerrors.Errorf(
|
|
"resolve computer use model for provider %q model %q: %w",
|
|
computerUseProvider,
|
|
computerUseModelName,
|
|
err,
|
|
)
|
|
}
|
|
|
|
return model, debugEnabled, resolvedProvider, resolvedModel, nil
|
|
}
|
|
|
|
type computerUseProviderToolOptions struct {
|
|
provider codersdk.ChatComputerUseProvider
|
|
isPlanModeTurn bool
|
|
isComputerUse bool
|
|
getWorkspaceConn func(context.Context) (workspacesdk.AgentConn, error)
|
|
storeFile chattool.StoreFileFunc
|
|
clock quartz.Clock
|
|
logger slog.Logger
|
|
}
|
|
|
|
func appendComputerUseProviderTool(
|
|
providerTools []chatloop.ProviderTool,
|
|
opts computerUseProviderToolOptions,
|
|
) ([]chatloop.ProviderTool, error) {
|
|
// This helper is called for every chat turn. Only chats created by the
|
|
// computer_use subagent definition have ChatModeComputerUse, which filters
|
|
// out root, general, and explore chats. Plan mode is separate from Mode, so
|
|
// planning turns stay gated even for computer-use chats.
|
|
if opts.isPlanModeTurn || !opts.isComputerUse {
|
|
return providerTools, nil
|
|
}
|
|
|
|
desktopGeometry := chattool.DefaultComputerUseDesktopGeometry(opts.provider)
|
|
definition, err := chattool.ComputerUseProviderTool(
|
|
opts.provider,
|
|
desktopGeometry.DeclaredWidth,
|
|
desktopGeometry.DeclaredHeight,
|
|
)
|
|
if err != nil {
|
|
return providerTools, xerrors.Errorf(
|
|
"build computer use provider tool for provider %q: %w",
|
|
opts.provider,
|
|
err,
|
|
)
|
|
}
|
|
|
|
clock := opts.clock
|
|
if clock == nil {
|
|
clock = quartz.NewReal()
|
|
}
|
|
providerTool := chatloop.ProviderTool{
|
|
Definition: definition,
|
|
Runner: chattool.NewComputerUseTool(
|
|
opts.provider,
|
|
desktopGeometry.DeclaredWidth,
|
|
desktopGeometry.DeclaredHeight,
|
|
opts.getWorkspaceConn,
|
|
opts.storeFile,
|
|
clock,
|
|
opts.logger,
|
|
),
|
|
}
|
|
if opts.provider == codersdk.ChatComputerUseProviderOpenAI {
|
|
// OpenAI computer-use image results need detail metadata so the model receives
|
|
// the screenshot at original detail when the chat loop sends the tool result.
|
|
providerTool.ResultProviderMetadata = openaicomputeruse.ResultProviderMetadata
|
|
}
|
|
|
|
return append(providerTools, providerTool), nil
|
|
}
|