mirror of
https://github.com/coder/coder.git
synced 2026-10-31 19:36:41 +08:00
Centralizes every LLM client build in chatd behind one resolver and makes all model calls behave the same, instead of preserving per-flow quirks. ## Why `chatd` built LLM clients in five places (standard turn, compaction override, quickgen, title override, advisor), each repeating config parsing, route resolution, client construction, and debug wrapping with slightly different behavior. An earlier iteration of this PR centralized construction but preserved every historical quirk behind purpose-specific spec constructors and policy flags. Review concluded the quirks themselves were the problem. ## What changed - `Server.resolveModelCall` (`modelcall.go`) is the single pipeline from a `modelCallSpec` to a ready client plus call metadata. Callers build spec literals inline; the only spec fields are data: model source (chat config, explicit config row, or fixed provider/model pair for computer use), requested reasoning effort, chatd-scoped routing, and build options. - Uniform behavior for every call: - Provider options are always derived. Whole-chat summaries, turn status labels, and chat-model compaction summaries previously sent none; they now follow the config like everything else. - `MaxOutputTokens` always defaults to 32k when the config leaves it unset. The compaction summary call nulls the cap after copying the template because it is non-streaming and the Anthropic SDK rejects non-streaming requests whose cap implies a completion over ten minutes. - Debug recording always wraps the client when chat debug is enabled, labeled with the resolved provider/model. The debug-rebuild paths (`manualTitleDebugSpec`, `quickgenDebugSpec`) are deleted; debug helpers now only create and finalize run records, since run association flows through the generate context, not the model. - Clients are always constructed with the configured model string and unmutated route, so AI Gateway validation sees the name exactly as configured. The resolved identity feeds metadata, logs, and debug labels only. - `resolvedModelCall.newCall` / `newObjectCall` are the only production constructors of `fantasy.Call` / `fantasy.ObjectCall`; object calls now carry the config's sampling parameters. - Net: -240 lines. ## Validation - Full `./coderd/x/chatd/...` suite green (PostgreSQL-backed). - Red-green toggles verified for the three key unifications (option derivation, token default, summary cap removal). - ARCHITECTURE.md: prose the refactor made stale is deleted; no new resolver documentation is added. > Mux acted on Mike's behalf for this PR.
117 lines
3.6 KiB
Go
117 lines
3.6 KiB
Go
package chatd
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
"cdr.dev/slog/v3"
|
|
"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/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
|
|
}
|
|
|
|
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
|
|
}
|