refactor: deprecate AIGatewayRoutingEnabled, remove direct chat routing (#26862)

This PR removes the now-dead direct-routing code:

- Deletes the direct routing implementation.
- Collapses the resolvedModelRoute discriminated union into aiGatewayModelRoute.
- Removes the dead providerKeys cascade.
- Deletes the preferredShortTextCandidates quickgen function.
- Simplifies the advisor override error handling.
- Deprecates the AIGatewayRoutingEnabled deployment option. It is now a no-op so as to not break existing deployments on upgrade.

Once direct routing was gone, the AI Gateway became mandatory for chat, which surfaced gaps in how the product behaves with the gateway disabled:

- Exposes ai-gateway-enabled to the frontend via embedded page metadata.
- Disables the chat composer via the existing AgentSetupNotice when the gateway is disabled, for both new and existing chats.
- Fixes nil/typed-nil chatDaemon panics on startup and shutdown when gateway is disabled.
- Fixes chat WebSocket from retrying the still-gated stream endpoint forever when the gateway is disabled.
This commit is contained in:
Cian Johnston
2026-07-01 20:15:03 +01:00
committed by GitHub
parent 3e0875d236
commit 4936ff9808
47 changed files with 1075 additions and 869 deletions
+56 -34
View File
@@ -714,6 +714,7 @@ func New(options *Options) *API {
Telemetry: options.Telemetry,
Logger: options.Logger.Named("site"),
HideAITasks: options.DeploymentValues.HideAITasks.Value(),
AIGatewayEnabled: options.DeploymentValues.AI.BridgeConfig.Enabled.Value(),
})
if err != nil {
options.Logger.Fatal(ctx, "failed to initialize site handler", slog.Error(err))
@@ -825,37 +826,38 @@ func New(options *Options) *API {
providerAPIKeys = *options.ChatProviderAPIKeys
}
chatAIGatewayRoutingEnabled := options.DeploymentValues.AI.BridgeConfig.Enabled.Value() &&
options.DeploymentValues.AI.Chat.AIGatewayRoutingEnabled.Value()
api.chatDaemon = chatd.New(options.Pubsub, chatd.Config{
Logger: options.Logger.Named("chatd"),
Database: options.Database,
ReplicaID: api.ID,
StreamPartsDialer: options.ChatStreamPartsDialer,
MaxChatsPerAcquire: int32(maxChatsPerAcquire), //nolint:gosec // maxChatsPerAcquire is clamped to int32 range above.
ProviderAPIKeys: providerAPIKeys,
AllowBYOK: options.DeploymentValues.AI.BridgeConfig.AllowBYOK.Value(),
AllowBYOKSet: true,
AIBridgeTransportFactory: &api.AIBridgeTransportFactory,
AIGatewayRoutingEnabled: chatAIGatewayRoutingEnabled,
AlwaysEnableDebugLogs: options.DeploymentValues.AI.Chat.DebugLoggingEnabled.Value(),
Experiments: experiments,
AgentConn: api.agentProvider.AgentConn,
AgentInactiveDisconnectTimeout: api.AgentInactiveDisconnectTimeout,
InstructionLookupTimeout: options.ChatdInstructionLookupTimeout,
CreateWorkspace: api.chatCreateWorkspace,
StartWorkspace: api.chatStartWorkspace,
StopWorkspace: api.chatStopWorkspace,
WebpushDispatcher: options.WebPushDispatcher,
UsageTracker: options.WorkspaceUsageTracker,
PrometheusRegistry: options.PrometheusRegistry,
OIDCTokenSource: oidcMCPSrc,
NotificationsEnqueuer: options.NotificationsEnqueuer,
Auditor: &api.Auditor,
})
if !options.ChatWorkerDisabled {
api.chatDaemon.Start()
// AI Gateway is mandatory for chat. When the bridge is disabled
// the chat daemon stays nil and chat HTTP handlers return a
// service-unavailable error with a clear remediation message.
if options.DeploymentValues.AI.BridgeConfig.Enabled.Value() {
api.chatDaemon = chatd.New(options.Pubsub, chatd.Config{
Logger: options.Logger.Named("chatd"),
Database: options.Database,
ReplicaID: api.ID,
StreamPartsDialer: options.ChatStreamPartsDialer,
MaxChatsPerAcquire: int32(maxChatsPerAcquire), //nolint:gosec // maxChatsPerAcquire is clamped to int32 range above.
ProviderAPIKeys: providerAPIKeys,
AllowBYOK: options.DeploymentValues.AI.BridgeConfig.AllowBYOK.Value(),
AllowBYOKSet: true,
AIBridgeTransportFactory: &api.AIBridgeTransportFactory,
AlwaysEnableDebugLogs: options.DeploymentValues.AI.Chat.DebugLoggingEnabled.Value(),
Experiments: experiments,
AgentConn: api.agentProvider.AgentConn,
AgentInactiveDisconnectTimeout: api.AgentInactiveDisconnectTimeout,
InstructionLookupTimeout: options.ChatdInstructionLookupTimeout,
CreateWorkspace: api.chatCreateWorkspace,
StartWorkspace: api.chatStartWorkspace,
StopWorkspace: api.chatStopWorkspace,
WebpushDispatcher: options.WebPushDispatcher,
UsageTracker: options.WorkspaceUsageTracker,
PrometheusRegistry: options.PrometheusRegistry,
OIDCTokenSource: oidcMCPSrc,
NotificationsEnqueuer: options.NotificationsEnqueuer,
Auditor: &api.Auditor,
})
if !options.ChatWorkerDisabled {
api.chatDaemon.Start()
}
}
gitSyncLogger := options.Logger.Named("gitsync")
refresher := gitsync.NewRefresher(
@@ -864,9 +866,10 @@ func New(options *Options) *API {
gitSyncLogger.Named("refresher"),
quartz.NewReal(),
)
publishDiffStatusChange := chatDaemonPublishDiffStatusChangeFunc(api.chatDaemon)
api.gitSyncWorker = gitsync.NewWorker(options.Database,
refresher,
api.chatDaemon.PublishDiffStatusChange,
publishDiffStatusChange,
quartz.NewReal(),
gitSyncLogger,
)
@@ -2312,6 +2315,23 @@ type API struct {
workspaceAgentConnWatcher *workspaceconnwatcher.Watcher
}
// chatDaemonPublishDiffStatusChangeFunc returns chatDaemon's
// PublishDiffStatusChange method bound as a gitsync.PublishDiffStatusChangeFunc,
// or a true nil func value when chatDaemon is nil (AI Gateway disabled).
//
// This must not be inlined as chatDaemon.PublishDiffStatusChange: a method
// value on a nil pointer receiver is itself non-nil (it captures the
// receiver, it doesn't call the method), so gitsync.Worker's own "if
// publishDiffStatusChangeFn != nil" check would not catch a nil chatDaemon,
// and invoking the returned func would panic dereferencing the nil
// receiver.
func chatDaemonPublishDiffStatusChangeFunc(chatDaemon *chatd.Server) gitsync.PublishDiffStatusChangeFunc {
if chatDaemon == nil {
return nil
}
return chatDaemon.PublishDiffStatusChange
}
// Close waits for all WebSocket connections to drain before returning.
func (api *API) Close() error {
select {
@@ -2346,8 +2366,10 @@ func (api *API) Close() error {
api.Logger.Warn(context.Background(),
"chat diff refresh worker did not exit in time")
}
if err := api.chatDaemon.Close(); err != nil {
api.Logger.Warn(api.ctx, "close chat processor", slog.Error(err))
if api.chatDaemon != nil {
if err := api.chatDaemon.Close(); err != nil {
api.Logger.Warn(api.ctx, "close chat processor", slog.Error(err))
}
}
api.metricsCache.Close()
if api.updateChecker != nil {