mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
8b1705eb65
## Summary Routes chatd model calls backed by concrete AI Provider rows through the in-process aibridge transport by default, with deployment options to use direct provider routing when AI Gateway is disabled or chat AI Gateway routing is disabled. - Splits model routing into common, direct provider, and AI Gateway paths behind a single deployment-mode entry point. - Builds chatd models through explicit request, route, and options data. Active API key attribution is passed explicitly instead of being hidden inside generic model construction. - For AI Gateway BYOK routes, resolves the user's provider key in chatd, forwards it through provider-specific auth headers, and sets `X-Coder-AI-Governance-Token` to the `delegated` marker so aibridge preserves those headers while still stripping Coder-specific metadata. - Keeps central provider credentials and deployment fallback credentials out of forwarded provider auth headers, so AI Gateway central policy remains authoritative. - Redacts delegated provider auth from default string formatting to avoid accidental plaintext logging of user BYOK credentials. - Covers selected chat models, advisor overrides, title and quickgen paths, subagent overrides, computer use model selection, and an integration-style chat turn through the aibridge transport path. - Persists initiating API key IDs on chat and queued user messages, including subagent child messages, and fails closed for AI Gateway-routed model builds without an active key. - Removes unused `api_key_id` indexes while keeping the persistence columns and foreign keys. - Keeps the deployment option available through config and env parsing, but hides it from CLI help and generated docs. - Stabilizes the subagent poll fallback test so background CreateChat processing cannot win the state transition under slower CI environments. ## Tests - `go test ./coderd/x/chatd -run 'TestAIGatewayProviderAuthForUser|TestAIGatewayProviderAuthRedactsFormatting|TestResolveModelRouteForConfigAIGatewayProviderAuth|TestAIGatewayModelForwardsProviderAuth|TestProcessChat_AIGatewayRoutingUsesDelegatedAPIKey|TestAwaitSubagentCompletion' -count=1` - `go test ./coderd/aibridged -run 'TestServeHTTP_DelegatedAPIKey|TestServeHTTP_StripCoderToken' -count=1` - `git diff --check HEAD~1..HEAD` - `make lint` > Mux working on behalf of Mike.
166 lines
4.8 KiB
Go
166 lines
4.8 KiB
Go
package chatd
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"charm.land/fantasy"
|
|
"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/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, 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 = strings.TrimSpace(rawProvider)
|
|
if provider == "" {
|
|
provider = chattool.ComputerUseProviderAnthropic
|
|
}
|
|
|
|
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 resolvedModelRoute,
|
|
computerUseProvider string,
|
|
computerUseModelProvider string,
|
|
computerUseModelName string,
|
|
modelOpts modelBuildOptions,
|
|
) (
|
|
model fantasy.LanguageModel,
|
|
debugEnabled bool,
|
|
resolvedProvider string,
|
|
resolvedModel string,
|
|
err error,
|
|
) {
|
|
resolvedProvider, resolvedModel, err = chatprovider.ResolveModelWithProviderHint(
|
|
computerUseModelName,
|
|
computerUseModelProvider,
|
|
)
|
|
if err != nil {
|
|
return nil, 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 nil, 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 string
|
|
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 == chattool.ComputerUseProviderOpenAI {
|
|
// 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
|
|
}
|