feat: add user AI spend endpoint (#26978)

## Description

Adds the `GET /api/v2/users/{user}/ai/spend` endpoint returning the
user's current AI spend, effective budget, and period bounds.

## Changes

- Add `userAISpendStatus` handler under the same feature/experiment gate
as `/api/v2/users/{user}/ai/budget`.
- Add `codersdk.UserAIBudgetSummary` (embedded into `UserAISpendStatus`)
and a `UserAISpendStatus` client method.
- Move `LimitSource` from `coderd/aibridge/budget` to `codersdk` so the
type is shared across endpoints.

Closes https://linear.app/codercom/issue/AIGOV-472

> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
This commit is contained in:
Susana Ferreira
2026-07-08 12:46:47 +01:00
committed by GitHub
parent 60d0859bde
commit 48f07e6e13
11 changed files with 719 additions and 45 deletions
+65 -2
View File
@@ -16,6 +16,7 @@ import (
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/coderd"
agplaibridge "github.com/coder/coder/v2/coderd/aibridge"
"github.com/coder/coder/v2/coderd/aibridge/budget"
"github.com/coder/coder/v2/coderd/audit"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/db2sdk"
@@ -586,7 +587,7 @@ func (api *API) groupAIBudget(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
group := httpmw.GroupParam(r)
budget, err := api.Database.GetGroupAIBudget(ctx, group.ID)
groupBudget, err := api.Database.GetGroupAIBudget(ctx, group.ID)
if httpapi.Is404Error(err) {
httpapi.ResourceNotFound(rw)
return
@@ -597,7 +598,7 @@ func (api *API) groupAIBudget(rw http.ResponseWriter, r *http.Request) {
return
}
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.GroupAIBudget(budget))
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.GroupAIBudget(groupBudget))
}
// @Summary Upsert group AI budget
@@ -875,3 +876,65 @@ func (api *API) deleteUserAIBudgetOverride(rw http.ResponseWriter, r *http.Reque
rw.WriteHeader(http.StatusNoContent)
}
// @Summary Get user AI spend
// @ID get-user-ai-spend
// @Security CoderSessionToken
// @Produce json
// @Tags Enterprise
// @Param user path string true "User ID, username, or me"
// @Success 200 {object} codersdk.UserAISpendStatus
// @Router /api/v2/users/{user}/ai/spend [get]
func (api *API) userAISpendStatus(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user := httpmw.UserParam(r)
logger := api.Logger.With(slog.F("user_id", user.ID))
periodWindow, err := budget.CurrentPeriod(api.Clock.Now(), codersdk.AIBudgetPeriodMonth)
if err != nil {
logger.Error(ctx, "failed to compute AI budget period", slog.Error(err))
httpapi.InternalServerError(rw, err)
return
}
logger = logger.With(
slog.F("period_start", periodWindow.Start),
slog.F("period_end", periodWindow.End),
)
policy := codersdk.NewAIBudgetPolicyFromString(api.DeploymentValues.AI.BridgeConfig.BudgetPolicy)
effectiveBudget, ok, err := budget.ResolveUserAIBudget(ctx, api.Database, user.ID, policy)
if err != nil {
logger.Error(ctx, "failed to resolve user AI budget", slog.Error(err))
httpapi.InternalServerError(rw, err)
return
}
resp := codersdk.UserAISpendStatus{
UserAIBudgetSummary: codersdk.UserAIBudgetSummary{
UserID: user.ID,
},
PeriodStart: periodWindow.Start,
PeriodEnd: periodWindow.End,
}
if ok {
resp.EffectiveGroupID = &effectiveBudget.GroupID
resp.SpendLimitMicros = &effectiveBudget.SpendLimitMicros
resp.LimitSource = &effectiveBudget.Source
logger = logger.With(slog.F("effective_group_id", effectiveBudget.GroupID))
spend, err := api.Database.GetUserAISpendSince(ctx, database.GetUserAISpendSinceParams{
UserID: user.ID,
EffectiveGroupID: effectiveBudget.GroupID,
PeriodStart: periodWindow.Start,
})
if err != nil {
logger.Error(ctx, "failed to get user AI spend", slog.Error(err))
httpapi.InternalServerError(rw, err)
return
}
resp.CurrentSpendMicros = spend.SpendMicros
}
httpapi.Write(ctx, rw, http.StatusOK, resp)
}
+272 -18
View File
@@ -19,13 +19,16 @@ import (
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/database/dbtime"
"github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/util/ptr"
"github.com/coder/coder/v2/codersdk"
entaudit "github.com/coder/coder/v2/enterprise/audit"
"github.com/coder/coder/v2/enterprise/audit/backends"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
"github.com/coder/coder/v2/enterprise/coderd/license"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
"github.com/coder/serpent"
)
@@ -2337,7 +2340,7 @@ func TestUserAIBudgetOverride(t *testing.T) {
t.Run("Upsert/CreatesAndUpdates", func(t *testing.T) {
t.Parallel()
adminClient, targetUser, group := setupUserAIBudgetOverrideTest(t)
adminClient, targetUser, group := setupAICostControlTest(t, aiCostControlTestOptions{GroupName: "override-test-group"})
ctx := testutil.Context(t, testutil.WaitLong)
// First upsert creates the override.
@@ -2367,7 +2370,7 @@ func TestUserAIBudgetOverride(t *testing.T) {
t.Run("Upsert/ReassignsGroup", func(t *testing.T) {
t.Parallel()
adminClient, targetUser, groupA := setupUserAIBudgetOverrideTest(t)
adminClient, targetUser, groupA := setupAICostControlTest(t, aiCostControlTestOptions{GroupName: "override-test-group"})
ctx := testutil.Context(t, testutil.WaitLong)
// First upsert: attribute spend to groupA.
@@ -2404,7 +2407,7 @@ func TestUserAIBudgetOverride(t *testing.T) {
t.Run("Upsert/EveryoneGroup", func(t *testing.T) {
t.Parallel()
adminClient, targetUser, _ := setupUserAIBudgetOverrideTest(t)
adminClient, targetUser, _ := setupAICostControlTest(t, aiCostControlTestOptions{GroupName: "override-test-group"})
ctx := testutil.Context(t, testutil.WaitLong)
// The Everyone group has id == organization_id, and the target user
@@ -2427,7 +2430,7 @@ func TestUserAIBudgetOverride(t *testing.T) {
t.Run("Upsert/AcceptsZeroSpendLimit", func(t *testing.T) {
t.Parallel()
adminClient, targetUser, group := setupUserAIBudgetOverrideTest(t)
adminClient, targetUser, group := setupAICostControlTest(t, aiCostControlTestOptions{GroupName: "override-test-group"})
ctx := testutil.Context(t, testutil.WaitLong)
// 0 is a valid value: it blocks all spend for the user.
@@ -2442,7 +2445,7 @@ func TestUserAIBudgetOverride(t *testing.T) {
t.Run("Upsert/RejectsNegativeSpend", func(t *testing.T) {
t.Parallel()
adminClient, targetUser, group := setupUserAIBudgetOverrideTest(t)
adminClient, targetUser, group := setupAICostControlTest(t, aiCostControlTestOptions{GroupName: "override-test-group"})
ctx := testutil.Context(t, testutil.WaitLong)
_, err := adminClient.UpsertUserAIBudgetOverride(ctx, targetUser.ID, codersdk.UpsertUserAIBudgetOverrideRequest{
@@ -2457,7 +2460,7 @@ func TestUserAIBudgetOverride(t *testing.T) {
t.Run("Upsert/RejectsUnknownGroup", func(t *testing.T) {
t.Parallel()
adminClient, targetUser, _ := setupUserAIBudgetOverrideTest(t)
adminClient, targetUser, _ := setupAICostControlTest(t, aiCostControlTestOptions{GroupName: "override-test-group"})
ctx := testutil.Context(t, testutil.WaitLong)
// A group_id that doesn't exist (or that the caller can't see)
@@ -2474,7 +2477,7 @@ func TestUserAIBudgetOverride(t *testing.T) {
t.Run("Upsert/RejectsNonMemberGroup", func(t *testing.T) {
t.Parallel()
adminClient, targetUser, _ := setupUserAIBudgetOverrideTest(t)
adminClient, targetUser, _ := setupAICostControlTest(t, aiCostControlTestOptions{GroupName: "override-test-group"})
ctx := testutil.Context(t, testutil.WaitLong)
// Create a second group the target is NOT a member of.
@@ -2495,7 +2498,7 @@ func TestUserAIBudgetOverride(t *testing.T) {
t.Run("Get/AbsentReturns404", func(t *testing.T) {
t.Parallel()
adminClient, targetUser, _ := setupUserAIBudgetOverrideTest(t)
adminClient, targetUser, _ := setupAICostControlTest(t, aiCostControlTestOptions{GroupName: "override-test-group"})
ctx := testutil.Context(t, testutil.WaitLong)
_, err := adminClient.UserAIBudgetOverride(ctx, targetUser.ID)
@@ -2507,7 +2510,7 @@ func TestUserAIBudgetOverride(t *testing.T) {
t.Run("Get/UnknownUserReturns404", func(t *testing.T) {
t.Parallel()
adminClient, _, _ := setupUserAIBudgetOverrideTest(t)
adminClient, _, _ := setupAICostControlTest(t, aiCostControlTestOptions{GroupName: "override-test-group"})
ctx := testutil.Context(t, testutil.WaitLong)
_, err := adminClient.UserAIBudgetOverride(ctx, uuid.New())
@@ -2519,7 +2522,7 @@ func TestUserAIBudgetOverride(t *testing.T) {
t.Run("Delete/RoundTrip", func(t *testing.T) {
t.Parallel()
adminClient, targetUser, group := setupUserAIBudgetOverrideTest(t)
adminClient, targetUser, group := setupAICostControlTest(t, aiCostControlTestOptions{GroupName: "override-test-group"})
ctx := testutil.Context(t, testutil.WaitLong)
_, err := adminClient.UpsertUserAIBudgetOverride(ctx, targetUser.ID, codersdk.UpsertUserAIBudgetOverrideRequest{
@@ -2539,7 +2542,7 @@ func TestUserAIBudgetOverride(t *testing.T) {
t.Run("Delete/AbsentReturns404", func(t *testing.T) {
t.Parallel()
adminClient, targetUser, _ := setupUserAIBudgetOverrideTest(t)
adminClient, targetUser, _ := setupAICostControlTest(t, aiCostControlTestOptions{GroupName: "override-test-group"})
ctx := testutil.Context(t, testutil.WaitLong)
err := adminClient.DeleteUserAIBudgetOverride(ctx, targetUser.ID)
@@ -2974,10 +2977,185 @@ func TestUserAIBudgetOverrideDeletedOnMembershipRemoval(t *testing.T) {
})
}
// setupUserAIBudgetOverrideTest returns an Admin client, a target user, and a
// group the target user is a member of.
func setupUserAIBudgetOverrideTest(t *testing.T) (adminClient *codersdk.Client, targetUser codersdk.User, group codersdk.Group) {
t.Helper()
func TestUserAISpendStatus(t *testing.T) {
t.Parallel()
t.Run("RequiresLicenseFeature", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentAIGatewayCostControl)}
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{DeploymentValues: dv},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{},
},
})
ctx := testutil.Context(t, testutil.WaitLong)
//nolint:gocritic // Owner role is irrelevant here; the request is blocked before RBAC.
_, err := client.UserAISpendStatus(ctx, uuid.New())
var sdkErr *codersdk.Error
require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
})
t.Run("RequiresExperiment", func(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
client, _ := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{DeploymentValues: dv},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureAIBridge: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitLong)
//nolint:gocritic // Owner role is irrelevant here; the request is blocked before RBAC.
_, err := client.UserAISpendStatus(ctx, uuid.New())
var sdkErr *codersdk.Error
require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
})
tests := []struct {
name string
groupBudget *int64 // nil = no group budget configured
overrideLimit *int64 // nil = no user override configured
spent int64 // 0 = no spend seeded
wantHasEffectiveGroup bool
wantSpendLimitMicros *int64
wantLimitSource *codersdk.AIBudgetLimitSource
wantCurrentSpendMicros int64
}{
{
name: "NoEffectiveGroup",
wantHasEffectiveGroup: false,
wantSpendLimitMicros: nil,
wantLimitSource: nil,
wantCurrentSpendMicros: 0,
},
{
name: "GroupBudget/ZeroSpend",
groupBudget: ptr.Ref(int64(1_000_000_000)),
wantHasEffectiveGroup: true,
wantSpendLimitMicros: ptr.Ref(int64(1_000_000_000)),
wantLimitSource: ptr.Ref(codersdk.AIBudgetLimitSourceGroup),
},
{
name: "GroupBudget/PartialSpend",
groupBudget: ptr.Ref(int64(1_000_000_000)),
spent: 250_000_000,
wantHasEffectiveGroup: true,
wantSpendLimitMicros: ptr.Ref(int64(1_000_000_000)),
wantLimitSource: ptr.Ref(codersdk.AIBudgetLimitSourceGroup),
wantCurrentSpendMicros: 250_000_000,
},
{
name: "GroupBudget/SpendExceedsLimit",
groupBudget: ptr.Ref(int64(1_000_000_000)),
spent: 1_500_000_000,
wantHasEffectiveGroup: true,
wantSpendLimitMicros: ptr.Ref(int64(1_000_000_000)),
wantLimitSource: ptr.Ref(codersdk.AIBudgetLimitSourceGroup),
wantCurrentSpendMicros: 1_500_000_000,
},
{
name: "UserOverride/ZeroSpend",
groupBudget: ptr.Ref(int64(5_000_000_000)),
overrideLimit: ptr.Ref(int64(200_000_000)),
wantHasEffectiveGroup: true,
wantSpendLimitMicros: ptr.Ref(int64(200_000_000)),
wantLimitSource: ptr.Ref(codersdk.AIBudgetLimitSourceUserOverride),
},
{
name: "UserOverride/PartialSpend",
groupBudget: ptr.Ref(int64(5_000_000_000)),
overrideLimit: ptr.Ref(int64(200_000_000)),
spent: 50_000_000,
wantHasEffectiveGroup: true,
wantSpendLimitMicros: ptr.Ref(int64(200_000_000)),
wantLimitSource: ptr.Ref(codersdk.AIBudgetLimitSourceUserOverride),
wantCurrentSpendMicros: 50_000_000,
},
{
name: "UserOverride/SpendExceedsLimit",
groupBudget: ptr.Ref(int64(5_000_000_000)),
overrideLimit: ptr.Ref(int64(200_000_000)),
spent: 350_000_000,
wantHasEffectiveGroup: true,
wantSpendLimitMicros: ptr.Ref(int64(200_000_000)),
wantLimitSource: ptr.Ref(codersdk.AIBudgetLimitSourceUserOverride),
wantCurrentSpendMicros: 350_000_000,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
clock := quartz.NewMock(t)
db, ps := dbtestutil.NewDB(t)
adminClient, targetUser, group := setupAICostControlTest(t, aiCostControlTestOptions{
GroupName: "spend-test-group",
Clock: clock,
Database: db,
Pubsub: ps,
})
ctx := testutil.Context(t, testutil.WaitLong)
// Use fixed dates to keep the test deterministic.
clock.Set(time.Date(2026, time.March, 15, 12, 0, 0, 0, time.UTC))
wantPeriodStart := time.Date(2026, time.March, 1, 0, 0, 0, 0, time.UTC)
wantPeriodEnd := time.Date(2026, time.April, 1, 0, 0, 0, 0, time.UTC)
if tt.groupBudget != nil {
_, err := adminClient.UpsertGroupAIBudget(ctx, group.ID, codersdk.UpsertGroupAIBudgetRequest{
SpendLimitMicros: *tt.groupBudget,
})
require.NoError(t, err)
}
if tt.overrideLimit != nil {
_, err := adminClient.UpsertUserAIBudgetOverride(ctx, targetUser.ID, codersdk.UpsertUserAIBudgetOverrideRequest{
GroupID: group.ID,
SpendLimitMicros: *tt.overrideLimit,
})
require.NoError(t, err)
}
if tt.spent > 0 {
_, err := db.IncrementUserAIDailySpend(ctx, database.IncrementUserAIDailySpendParams{
UserID: targetUser.ID,
EffectiveGroupID: group.ID,
Day: clock.Now(),
CostMicros: tt.spent,
})
require.NoError(t, err)
}
got, err := adminClient.UserAISpendStatus(ctx, targetUser.ID)
require.NoError(t, err)
require.Equal(t, targetUser.ID, got.UserID)
require.Equal(t, wantPeriodStart, got.PeriodStart)
require.Equal(t, wantPeriodEnd, got.PeriodEnd)
require.Equal(t, tt.wantCurrentSpendMicros, got.CurrentSpendMicros)
var wantEffectiveGroupID *uuid.UUID
if tt.wantHasEffectiveGroup {
wantEffectiveGroupID = &group.ID
}
require.Equal(t, wantEffectiveGroupID, got.EffectiveGroupID)
require.Equal(t, tt.wantSpendLimitMicros, got.SpendLimitMicros)
require.Equal(t, tt.wantLimitSource, got.LimitSource)
})
}
}
func TestUserAISpendStatusRoleAccess(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
@@ -2991,12 +3169,88 @@ func setupUserAIBudgetOverrideTest(t *testing.T) (adminClient *codersdk.Client,
},
},
})
adminClient, _ = coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleUserAdmin())
_, targetUser = coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
userAdminClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleUserAdmin())
orgAdminClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.ScopedRoleOrgAdmin(owner.OrganizationID))
orgUserAdminClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.ScopedRoleOrgUserAdmin(owner.OrganizationID))
memberClient, memberUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
_, targetUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
cases := []struct {
Name string
Client *codersdk.Client
Target uuid.UUID
WantCode int
}{
{Name: "Owner", Client: ownerClient, Target: targetUser.ID, WantCode: http.StatusOK},
{Name: "UserAdmin", Client: userAdminClient, Target: targetUser.ID, WantCode: http.StatusOK},
{Name: "OrgAdmin", Client: orgAdminClient, Target: targetUser.ID, WantCode: http.StatusOK},
{Name: "OrgUserAdmin", Client: orgUserAdminClient, Target: targetUser.ID, WantCode: http.StatusOK},
{Name: "MemberReadsSelf", Client: memberClient, Target: memberUser.ID, WantCode: http.StatusOK},
{Name: "MemberReadsOther", Client: memberClient, Target: targetUser.ID, WantCode: http.StatusNotFound},
}
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
_, err := tc.Client.UserAISpendStatus(ctx, tc.Target)
if tc.WantCode == http.StatusOK {
require.NoError(t, err)
return
}
var sdkErr *codersdk.Error
require.ErrorAs(t, err, &sdkErr)
require.Equal(t, tc.WantCode, sdkErr.StatusCode())
})
}
}
// aiCostControlTestOptions configures the setup of an AI cost control test
// deployment. GroupName is required. Clock, Database, and Pubsub are
// optional overrides (leave nil for defaults).
type aiCostControlTestOptions struct {
GroupName string
Clock quartz.Clock
Database database.Store
Pubsub pubsub.Pubsub
}
// setupAICostControlTest builds a deployment with FeatureAIBridge licensed
// and the AI Gateway cost control experiment enabled, creates an admin
// client and target user, adds the target user to a group, and returns
// the admin client, target user, and group.
func setupAICostControlTest(t *testing.T, opts aiCostControlTestOptions) (*codersdk.Client, codersdk.User, codersdk.Group) {
t.Helper()
dv := coderdtest.DeploymentValues(t)
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
dv.Experiments = []string{string(codersdk.ExperimentAIGatewayCostControl)}
coderdOpts := &coderdtest.Options{DeploymentValues: dv}
if opts.Clock != nil {
coderdOpts.Clock = opts.Clock
}
if opts.Database != nil {
coderdOpts.Database = opts.Database
}
if opts.Pubsub != nil {
coderdOpts.Pubsub = opts.Pubsub
}
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
Options: coderdOpts,
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureTemplateRBAC: 1,
codersdk.FeatureAIBridge: 1,
},
},
})
adminClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleUserAdmin())
_, targetUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
ctx := testutil.Context(t, testutil.WaitLong)
g, err := adminClient.CreateGroup(ctx, owner.OrganizationID, codersdk.CreateGroupRequest{
Name: "override-test-group",
Name: opts.GroupName,
})
require.NoError(t, err)
g, err = adminClient.PatchGroup(ctx, g.ID, codersdk.PatchGroupRequest{
+9 -4
View File
@@ -641,7 +641,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
r.Get("/", api.userQuietHoursSchedule)
r.Put("/", api.putUserQuietHoursSchedule)
})
r.Route("/users/{user}/ai/budget", func(r chi.Router) {
r.Route("/users/{user}/ai", func(r chi.Router) {
// AI cost controls are a paid feature (AI Governance add-on).
r.Use(
// TODO(AIGOV-443): remove once AI Gateway cost control functionality is stable.
@@ -650,9 +650,14 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
apiKeyMiddleware,
httpmw.ExtractUserParam(options.Database),
)
r.Get("/", api.userAIBudgetOverride)
r.Put("/", api.upsertUserAIBudgetOverride)
r.Delete("/", api.deleteUserAIBudgetOverride)
r.Route("/budget", func(r chi.Router) {
r.Get("/", api.userAIBudgetOverride)
r.Put("/", api.upsertUserAIBudgetOverride)
r.Delete("/", api.deleteUserAIBudgetOverride)
})
r.Route("/spend", func(r chi.Router) {
r.Get("/", api.userAISpendStatus)
})
})
r.Route("/prebuilds", func(r chi.Router) {
r.Use(