Files
coder/coderd/x/chatd/computer_use.go
T
Michael Suchacz 8b1705eb65 feat: route chatd provider traffic through aibridge (#25629)
## 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.
2026-05-26 19:31:52 +00:00

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
}