feat: track ai seat usage (#22682)

When a user uses an AI feature, we record them in the `ai_seat_state` as consuming a seat. 

Added in debouching to prevent excessive writes to the db for this feature. There is no need for frequent updates.
This commit is contained in:
Steven Masley
2026-03-16 12:36:26 -05:00
committed by GitHub
parent cabb611fd9
commit abf59ee7a6
10 changed files with 262 additions and 7 deletions
@@ -28,6 +28,7 @@ import (
protobuf "google.golang.org/protobuf/proto"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/coderd/aiseats"
"github.com/coder/coder/v2/coderd/apikey"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
@@ -76,6 +77,7 @@ const (
type Options struct {
OIDCConfig promoauth.OAuth2Config
ExternalAuthConfigs []*externalauth.Config
AISeatTracker aiseats.SeatTracker
// Clock for testing
Clock quartz.Clock
@@ -120,6 +122,7 @@ type server struct {
NotificationsEnqueuer notifications.Enqueuer
PrebuildsOrchestrator *atomic.Pointer[prebuilds.ReconciliationOrchestrator]
UsageInserter *atomic.Pointer[usage.Inserter]
AISeatTracker aiseats.SeatTracker
Experiments codersdk.Experiments
OIDCConfig promoauth.OAuth2Config
@@ -215,6 +218,9 @@ func NewServer(
if err := tags.Valid(); err != nil {
return nil, xerrors.Errorf("invalid tags: %w", err)
}
if options.AISeatTracker == nil {
options.AISeatTracker = aiseats.Noop{}
}
if options.AcquireJobLongPollDur == 0 {
options.AcquireJobLongPollDur = DefaultAcquireJobLongPollDur
}
@@ -253,6 +259,7 @@ func NewServer(
heartbeatFn: options.HeartbeatFn,
PrebuildsOrchestrator: prebuildsOrchestrator,
UsageInserter: usageInserter,
AISeatTracker: options.AISeatTracker,
metrics: metrics,
Experiments: experiments,
}
@@ -2437,6 +2444,12 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
})
}
// Record AI seat usage for successful task workspace builds.
if workspaceBuild.Transition == database.WorkspaceTransitionStart && workspace.TaskID.Valid {
s.AISeatTracker.RecordUsage(ctx, workspace.OwnerID,
aiseats.ReasonTask("task workspace build succeeded"))
}
if s.PrebuildsOrchestrator != nil && input.PrebuiltWorkspaceBuildStage == sdkproto.PrebuiltWorkspaceBuildStage_CLAIM {
// Track resource replacements, if there are any.
orchestrator := s.PrebuildsOrchestrator.Load()