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

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
239eaf0
feat: notify users when AI spend crosses the budget threshold
evgeniy-scherbina Jul 20, 2026
90ab015
fix: show AI budget notification in user settings and clarify its tex…
evgeniy-scherbina Jul 21, 2026
dd514ae
Merge remote-tracking branch 'origin/main' into yevhenii/cost-control…
evgeniy-scherbina Jul 21, 2026
8dc829a
chore: fix migration numbers
evgeniy-scherbina Jul 21, 2026
c40737a
chore: shorten AI budget warning notification name to AI Budget Warning
evgeniy-scherbina Jul 22, 2026
05bc337
chore: regenerate webhook golden for AI budget warning rename
evgeniy-scherbina Jul 22, 2026
6aa58d1
fix: derive AI budget period from interception createdAt
evgeniy-scherbina Jul 22, 2026
98ed750
test: cover AI budget notification across period boundary
evgeniy-scherbina Jul 22, 2026
aadb31f
refactor: scope AI budget notifications to least privilege (#27433)
evgeniy-scherbina Jul 22, 2026
6173158
feat: template AI budget period in notifications (#27434)
evgeniy-scherbina Jul 22, 2026
6fd2bb7
fix: only name the group when it set the AI budget limit (#27436)
evgeniy-scherbina Jul 23, 2026
4d05f29
test: add threshold label to AI budget limit golden
evgeniy-scherbina Jul 23, 2026
3720ea0
chore: rename AI budget notification group to AI Budget Events
evgeniy-scherbina Jul 23, 2026
da5a88d
fix: minor fixes
evgeniy-scherbina Jul 23, 2026
2515e0a
fix: CR's fixes
evgeniy-scherbina Jul 23, 2026
9a04f6b
chore: rename AI notification group to AI Cost Control Events
evgeniy-scherbina Jul 23, 2026
d3f659d
chore: use effective-group naming in AI budget notifications
evgeniy-scherbina Jul 23, 2026
fc54c0c
Merge remote-tracking branch 'origin/main' into yevhenii/cost-control…
evgeniy-scherbina Jul 23, 2026
733ce14
chore: rename template field and clarify period comment
evgeniy-scherbina Jul 23, 2026
aadcf0c
fix: drop limit_source from AI budget notifications, keep group (#27467)
evgeniy-scherbina Jul 23, 2026
5218335
test: merge budget warning and limit notification tests
evgeniy-scherbina Jul 24, 2026
0e7d916
test: express budget test amounts in dollar units
evgeniy-scherbina Jul 24, 2026
cd98487
test: cover best-effort budget notification failures
evgeniy-scherbina Jul 24, 2026
25f871a
test: assert zero budget limit sends no notification
evgeniy-scherbina Jul 24, 2026
7eeb49a
feat: show budget period window in AI budget notifications (#27489)
evgeniy-scherbina Jul 24, 2026
137e8cd
Update coderd/aibridgedserver/aibridgedserver.go
evgeniy-scherbina Jul 27, 2026
3b512ad
Merge remote-tracking branch 'origin/main' into yevhenii/cost-control…
evgeniy-scherbina Jul 27, 2026
bef2d0a
ci: run make fmt
evgeniy-scherbina Jul 27, 2026
8c5a1f3
Merge remote-tracking branch 'origin/main' into yevhenii/cost-control…
evgeniy-scherbina Jul 27, 2026
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
1 change: 1 addition & 0 deletions coderd/aibridged.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func (api *API) CreateInMemoryAIBridgeServer(dialCtx context.Context) (client ai
Store: api.Database,
Pubsub: api.Pubsub,
AISeatTracker: api.AISeatTracker,
Enqueuer: api.NotificationsEnqueuer,
AccessURL: api.AccessURL.String(),
GatewayCfg: api.DeploymentValues.AI.BridgeConfig,
ExternalAuthConfigs: api.ExternalAuthConfigs,
Expand Down
47 changes: 44 additions & 3 deletions coderd/aibridgedserver/aibridgedserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/coder/coder/v2/coderd/externalauth"
"github.com/coder/coder/v2/coderd/httpmw"
codermcp "github.com/coder/coder/v2/coderd/mcp"
"github.com/coder/coder/v2/coderd/notifications"
coderdpubsub "github.com/coder/coder/v2/coderd/pubsub"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
Expand Down Expand Up @@ -82,6 +83,7 @@ type store interface {
GetHighestGroupAIBudgetByUser(ctx context.Context, userID uuid.UUID) (database.GetHighestGroupAIBudgetByUserRow, error)
GetUserEveryoneFallbackGroup(ctx context.Context, userID uuid.UUID) (uuid.UUID, error)
GetUserAISpendSince(ctx context.Context, arg database.GetUserAISpendSinceParams) (database.GetUserAISpendSinceRow, error)
GetGroupByID(ctx context.Context, id uuid.UUID) (database.Group, error)

// MCPConfigurator-related queries.
GetExternalAuthLinksByUserID(ctx context.Context, userID uuid.UUID) ([]database.ExternalAuthLink, error)
Expand Down Expand Up @@ -117,15 +119,19 @@ type Server struct {
budgetPolicy codersdk.AIBudgetPolicy
// budgetPeriod is the deployment-configured budgeting period used to
// derive the window over which user AI spend is aggregated.
budgetPeriod codersdk.AIBudgetPeriod
clock quartz.Clock
budgetPeriod codersdk.AIBudgetPeriod
clock quartz.Clock
notifEnqueuer notifications.Enqueuer
}

// Options carries the dependencies required to construct an aibridged Server.
type Options struct {
Store store
Pubsub pubsub.Pubsub
AISeatTracker aiseats.SeatTracker
// Enqueuer enqueues notifications. When nil, NewServer substitutes a no-op
// enqueuer.
Enqueuer notifications.Enqueuer

AccessURL string
GatewayCfg codersdk.AIBridgeConfig
Expand All @@ -137,6 +143,11 @@ type Options struct {
}

func NewServer(lifecycleCtx context.Context, opts Options) (*Server, error) {
enqueuer := opts.Enqueuer
Comment thread
evgeniy-scherbina marked this conversation as resolved.
if enqueuer == nil {
enqueuer = notifications.NewNoopEnqueuer()
}

eac := make(map[string]*externalauth.Config, len(opts.ExternalAuthConfigs))

for _, cfg := range opts.ExternalAuthConfigs {
Expand All @@ -158,6 +169,7 @@ func NewServer(lifecycleCtx context.Context, opts Options) (*Server, error) {
budgetPolicy: codersdk.NewAIBudgetPolicyFromString(opts.GatewayCfg.BudgetPolicy),
budgetPeriod: codersdk.NewAIBudgetPeriodFromString(opts.GatewayCfg.BudgetPeriod),
clock: opts.Clock,
notifEnqueuer: enqueuer,
}

if opts.GatewayCfg.InjectCoderMCPTools {
Expand Down Expand Up @@ -357,7 +369,11 @@ func (s *Server) RecordTokenUsage(ctx context.Context, in *proto.RecordTokenUsag
// positive, accumulates that cost into the user's daily spend.
func (s *Server) recordTokenUsageAndSpend(ctx context.Context, intc database.AIBridgeInterception, cost tokenUsageCost, in *proto.RecordTokenUsageRequest, metadataJSON []byte) error {
createdAt := in.GetCreatedAt().AsTime()
return s.store.InTx(func(tx database.Store) error {

// Populated inside the transaction with any budget thresholds this
// interception crossed.
var crossings []budgetThresholdCrossing
err := s.store.InTx(func(tx database.Store) error {
if _, err := tx.InsertAIBridgeTokenUsage(ctx, database.InsertAIBridgeTokenUsageParams{
ID: uuid.New(),
InterceptionID: intc.ID,
Expand Down Expand Up @@ -400,8 +416,33 @@ func (s *Server) recordTokenUsageAndSpend(ctx context.Context, intc database.AIB
}); err != nil {
return xerrors.Errorf("increment user daily spend: %w", err)
}

// Threshold detection is best-effort: a failed read must not roll back
// the committed spend, so the error is logged rather than propagated.
Comment on lines +420 to +421

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we are testing this 🤔 this would be nice to have in the tests

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure this needed a test, but I added one anyway.

It still uses mockdb, but I think that's sufficient.

var detectErr error
crossings, detectErr = s.detectBudgetThresholdCrossings(ctx, tx, intc, cost, createdAt)
if detectErr != nil {
s.logger.Error(ctx, "failed to detect AI budget threshold crossing",
slog.F("interception_id", intc.ID),
slog.F("initiator_id", intc.InitiatorID),
slog.Error(detectErr))
}
return nil
}, nil)
if err != nil {
return err
}

for _, crossing := range crossings {
if err := s.notifyBudgetThresholdCrossing(ctx, crossing); err != nil {
Comment thread
evgeniy-scherbina marked this conversation as resolved.
s.logger.Error(ctx, "failed to send AI budget notification",
slog.F("user_id", crossing.userID),
slog.F("group_id", crossing.effectiveGroupID),
slog.F("threshold_percent", crossing.thresholdPercent),
slog.Error(err))
}
}
return nil
}

func (s *Server) RecordPromptUsage(ctx context.Context, in *proto.RecordPromptUsageRequest) (*proto.RecordPromptUsageResponse, error) {
Expand Down
Loading
Loading