Files
coder/coderd/x/chatd/model_routing.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

169 lines
4.2 KiB
Go

package chatd
import (
"context"
"net/http"
"charm.land/fantasy"
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/x/chatd/chatprovider"
)
type modelClientRequest struct {
Chat database.Chat
ModelName string
UserAgent string
ExtraHeaders map[string]string
}
type modelBuildOptions struct {
ActiveAPIKeyID string
RecordHTTP bool
}
func modelBuildOptionsFromMessages(messages []database.ChatMessage) modelBuildOptions {
apiKeyID, _ := activeTurnAPIKeyIDFromMessages(messages)
return modelBuildOptions{ActiveAPIKeyID: apiKeyID}
}
type modelRouteKind int
const (
modelRouteKindDirect modelRouteKind = iota + 1
modelRouteKindAIGateway
)
type resolvedModelRoute struct {
kind modelRouteKind
direct directModelRoute
aiGateway aiGatewayModelRoute
}
func newDirectModelRoute(providerHint string, keys chatprovider.ProviderAPIKeys) resolvedModelRoute {
return resolvedModelRoute{
kind: modelRouteKindDirect,
direct: directModelRoute{
ProviderHint: providerHint,
Keys: keys,
},
}
}
func (r resolvedModelRoute) providerHint() (string, error) {
switch r.kind {
case modelRouteKindDirect:
return r.direct.ProviderHint, nil
case modelRouteKindAIGateway:
return r.aiGateway.ModelProviderHint, nil
default:
return "", xerrors.New("model route is not configured")
}
}
func (r resolvedModelRoute) withProviderHint(providerHint string) resolvedModelRoute {
switch r.kind {
case modelRouteKindDirect:
r.direct.ProviderHint = providerHint
case modelRouteKindAIGateway:
r.aiGateway.ModelProviderHint = providerHint
}
return r
}
func (r resolvedModelRoute) directProviderKeys() chatprovider.ProviderAPIKeys {
if r.kind != modelRouteKindDirect {
return chatprovider.ProviderAPIKeys{}
}
return r.direct.Keys
}
func (p *Server) enabledAIProviderByID(ctx context.Context, providerID uuid.UUID) (database.AIProvider, error) {
provider, err := p.db.GetAIProviderByID(ctx, providerID)
if err != nil {
return database.AIProvider{}, xerrors.Errorf("get AI provider: %w", err)
}
if !provider.Enabled {
return database.AIProvider{}, xerrors.Errorf("AI provider %s is disabled", provider.ID)
}
return provider, nil
}
func (p *Server) shouldUseAIGatewayRouting() bool {
return p.aiGatewayRoutingEnabled
}
func (p *Server) resolveModelRouteForConfig(
ctx context.Context,
ownerID uuid.UUID,
modelConfig database.ChatModelConfig,
fallbackKeys chatprovider.ProviderAPIKeys,
) (resolvedModelRoute, error) {
if p.shouldUseAIGatewayRouting() {
return p.resolveAIGatewayModelRouteForConfig(ctx, ownerID, modelConfig)
}
return p.resolveDirectModelRouteForConfig(ctx, ownerID, modelConfig, fallbackKeys)
}
func (p *Server) resolveModelRouteForProviderType(
ctx context.Context,
ownerID uuid.UUID,
providerType string,
) (resolvedModelRoute, error) {
if p.shouldUseAIGatewayRouting() {
return p.resolveAIGatewayModelRouteForProviderType(ctx, ownerID, providerType)
}
return p.resolveDirectModelRouteForProviderType(ctx, ownerID, providerType)
}
func (p *Server) newModel(
ctx context.Context,
req modelClientRequest,
route resolvedModelRoute,
opts modelBuildOptions,
) (fantasy.LanguageModel, error) {
switch route.kind {
case modelRouteKindDirect:
return p.newDirectModel(ctx, req, route.direct, opts)
case modelRouteKindAIGateway:
return p.newAIGatewayModel(ctx, req, route.aiGateway, opts)
default:
return nil, xerrors.New("model route is not configured")
}
}
func newLanguageModel(
providerHint string,
modelName string,
providerKeys chatprovider.ProviderAPIKeys,
userAgent string,
extraHeaders map[string]string,
httpClient *http.Client,
) (fantasy.LanguageModel, error) {
model, err := chatprovider.ModelFromConfig(
providerHint,
modelName,
providerKeys,
userAgent,
extraHeaders,
httpClient,
)
if err != nil {
return nil, err
}
if model == nil {
provider, resolvedModel, resolveErr := chatprovider.ResolveModelWithProviderHint(modelName, providerHint)
if resolveErr != nil {
return nil, resolveErr
}
return nil, xerrors.Errorf(
"create model for %s/%s returned nil",
provider,
resolvedModel,
)
}
return model, nil
}