Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,24 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd.
cliui.Errorf(inv.Stderr, "Notify systemd failed: %s", err)
}

// Stop accepting new connections to aibridged.
//
// When running as an in-memory daemon, the HTTP handler is wired into the
// coderd API and therefore is subject to its context. Calling shutdown on
// aibridged will NOT affect in-flight requests but those will be closed once
// the API server is shutdown below.
if current := coderAPI.AIBridgeDaemon.Load(); current != nil {
cliui.Info(inv.Stdout, "Shutting down aibridge daemon...\n")

err = shutdownWithTimeout((*current).Shutdown, 5*time.Second)
if err != nil {
cliui.Errorf(inv.Stderr, "Graceful shutdown of aibridge daemon failed: %s\n", err)
} else {
_ = (*current).Close()
cliui.Info(inv.Stdout, "Gracefully shut down aibridge daemon\n")
}
}

// Stop accepting new connections without interrupting
// in-flight requests, give in-flight requests 5 seconds to
// complete.
Expand Down
83 changes: 82 additions & 1 deletion coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"sync/atomic"
"time"

"github.com/coder/coder/v2/aibridged"
aibridgedproto "github.com/coder/coder/v2/aibridged/proto"
"github.com/coder/coder/v2/coderd/oauth2provider"
"github.com/coder/coder/v2/coderd/pproflabel"
"github.com/coder/coder/v2/coderd/prebuilds"
Expand All @@ -44,6 +46,9 @@ import (
"tailscale.com/types/key"
"tailscale.com/util/singleflight"

"github.com/coder/coder/v2/coderd/aibridgedserver"
"github.com/coder/coder/v2/provisionerd/proto"

"cdr.dev/slog"
"github.com/coder/quartz"
"github.com/coder/serpent"
Expand Down Expand Up @@ -95,7 +100,6 @@ import (
"github.com/coder/coder/v2/coderd/workspacestats"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/healthsdk"
"github.com/coder/coder/v2/provisionerd/proto"
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/coder/v2/site"
"github.com/coder/coder/v2/tailnet"
Expand Down Expand Up @@ -632,6 +636,7 @@ func New(options *Options) *API {
api.PortSharer.Store(&portsharing.DefaultPortSharer)
api.PrebuildsClaimer.Store(&prebuilds.DefaultClaimer)
api.PrebuildsReconciler.Store(&prebuilds.DefaultReconciler)
api.AIBridgeDaemon.Store(&aibridged.DefaultServer)
buildInfo := codersdk.BuildInfoResponse{
ExternalURL: buildinfo.ExternalURL(),
Version: buildinfo.Version(),
Expand Down Expand Up @@ -1766,6 +1771,8 @@ type API struct {
// dbRolluper rolls up template usage stats from raw agent and app
// stats. This is used to provide insights in the WebUI.
dbRolluper *dbrollup.Rolluper

AIBridgeDaemon atomic.Pointer[aibridged.Server]
}

// Close waits for all WebSocket connections to drain before returning.
Expand Down Expand Up @@ -1824,6 +1831,10 @@ func (api *API) Close() error {
(*current).Stop(ctx, nil)
}

if current := api.AIBridgeDaemon.Load(); current != nil {
_ = (*current).Close()
}

return nil
}

Expand Down Expand Up @@ -1997,6 +2008,76 @@ func (api *API) CreateInMemoryTaggedProvisionerDaemon(dialCtx context.Context, n
return proto.NewDRPCProvisionerDaemonClient(clientSession), nil
}

func (api *API) CreateInMemoryAIBridgeDaemon(dialCtx context.Context) (client aibridged.DRPCClient, err error) {
// TODO(dannyk): implement options.
// TODO(dannyk): implement tracing.
// TODO(dannyk): implement API versioning.

clientSession, serverSession := drpcsdk.MemTransportPipe()
defer func() {
if err != nil {
_ = clientSession.Close()
_ = serverSession.Close()
}
}()

mux := drpcmux.New()
api.Logger.Debug(dialCtx, "starting in-memory aibridge daemon")
logger := api.Logger.Named("inmem-aibridged")
srv, err := aibridgedserver.NewServer(api.ctx, api.Database, logger,
api.DeploymentValues.AccessURL.String(), api.ExternalAuthConfigs)
if err != nil {
return nil, err
}
err = aibridgedproto.DRPCRegisterRecorder(mux, srv)
if err != nil {
return nil, xerrors.Errorf("register recorder service: %w", err)
}
err = aibridgedproto.DRPCRegisterMCPConfigurator(mux, srv)
if err != nil {
return nil, xerrors.Errorf("register MCP configurator service: %w", err)
}
err = aibridgedproto.DRPCRegisterAuthenticator(mux, srv)
if err != nil {
return nil, xerrors.Errorf("register authenticator service: %w", err)
}
server := drpcserver.NewWithOptions(&tracing.DRPCHandler{Handler: mux},
drpcserver.Options{
Manager: drpcsdk.DefaultDRPCOptions(nil),
Log: func(err error) {
if xerrors.Is(err, io.EOF) {
return
}
logger.Debug(dialCtx, "drpc server error", slog.Error(err))
},
},
)
// in-mem pipes aren't technically "websockets" but they have the same properties as far as the
// API is concerned: they are long-lived connections that we need to close before completing
// shutdown of the API.
api.WebsocketWaitMutex.Lock()
api.WebsocketWaitGroup.Add(1)
api.WebsocketWaitMutex.Unlock()
go func() {
defer api.WebsocketWaitGroup.Done()
// Here we pass the background context, since we want the server to keep serving until the
// client hangs up. The aibridged is local, in-mem, so there isn't a danger of losing contact with it and
// having a dead connection we don't know the status of.
err := server.Serve(context.Background(), serverSession)
logger.Info(dialCtx, "aibridge daemon disconnected", slog.Error(err))
// Close the sessions, so we don't leak goroutines serving them.
_ = clientSession.Close()
_ = serverSession.Close()
}()

return &aibridged.Client{
Conn: clientSession,
DRPCRecorderClient: aibridgedproto.NewDRPCRecorderClient(clientSession),
DRPCMCPConfiguratorClient: aibridgedproto.NewDRPCMCPConfiguratorClient(clientSession),
DRPCAuthenticatorClient: aibridgedproto.NewDRPCAuthenticatorClient(clientSession),
}, nil
}

func (api *API) DERPMap() *tailcfg.DERPMap {
fn := api.DERPMapper.Load()
if fn != nil {
Expand Down
45 changes: 45 additions & 0 deletions enterprise/coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"sync/atomic"
"time"

"github.com/coder/coder/v2/aibridged"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/coderd/appearance"
"github.com/coder/coder/v2/coderd/database"
Expand Down Expand Up @@ -61,6 +62,8 @@ import (
"github.com/coder/coder/v2/enterprise/tailnet"
"github.com/coder/coder/v2/provisionerd/proto"
agpltailnet "github.com/coder/coder/v2/tailnet"

"github.com/coder/aibridge"
)

// New constructs an Enterprise coderd API instance.
Expand Down Expand Up @@ -617,6 +620,20 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
return nil, xerrors.Errorf("unable to register license metrics collector")
}

// In-memory aibridge daemons.
// TODO: license entitlement.
if api.DeploymentValues.AI.BridgeConfig.Enabled {
if api.AGPL.Experiments.Enabled(codersdk.ExperimentAIBridge) {
srv, err := newAIBridgeServer(api.AGPL)
if err != nil {
return nil, xerrors.Errorf("create aibridged: %w", err)
}
api.AGPL.AIBridgeDaemon.Store(&srv)
} else {
api.Logger.Error(ctx, fmt.Sprintf("aibridge enabled but experiment %q not enabled", codersdk.ExperimentAIBridge))
}
}

err = api.updateEntitlements(ctx)
if err != nil {
return nil, xerrors.Errorf("update entitlements: %w", err)
Expand Down Expand Up @@ -1275,3 +1292,31 @@ func (api *API) setupPrebuilds(featureEnabled bool) (agplprebuilds.Reconciliatio
api.Logger.Named("prebuilds"), quartz.NewReal(), api.PrometheusRegistry, api.NotificationsEnqueuer, api.AGPL.BuildUsageChecker)
return reconciler, prebuilds.NewEnterpriseClaimer(api.Database)
}

func newAIBridgeServer(coderAPI *coderd.API) (aibridged.Server, error) {
srv, err := aibridged.New(
func(dialCtx context.Context) (aibridged.DRPCClient, error) {
return coderAPI.CreateInMemoryAIBridgeDaemon(dialCtx)
},
convertAIBridgeDeploymentValues(coderAPI.DeploymentValues.AI.BridgeConfig),
coderAPI.Logger.Named("aibridged"),
)
if err != nil {
return nil, xerrors.Errorf("create aibridge daemon: %w", err)
}
return srv, nil
}

func convertAIBridgeDeploymentValues(vals codersdk.AIBridgeConfig) aibridge.Config {
return aibridge.Config{
OpenAI: aibridge.ProviderConfig{
BaseURL: vals.OpenAI.BaseURL.String(),
Key: vals.OpenAI.Key.String(),
},
Anthropic: aibridge.ProviderConfig{
BaseURL: vals.Anthropic.BaseURL.String(),
Key: vals.Anthropic.Key.String(),
},
CacheSize: 100, // TODO: configurable.
}
}
Loading