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.
169 lines
4.2 KiB
Go
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
|
|
}
|