mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add per-group AI budget table and endpoints (#25203)
Closes https://linear.app/codercom/issue/AIGOV-284/add-group-budgets-table-and-crud-api ## Summary Adds the `group_ai_budgets` table and the following endpoints: - `GET /api/v2/groups/{group}/ai/budget` - `PUT /api/v2/groups/{group}/ai/budget` - `DELETE /api/v2/groups/{group}/ai/budget` Each group may have at most one budget row. If no row exists, no budget is enforced. ### Feature gate Added `RequireFeatureMW(FeatureAIBridge)` on the `/ai/budget` sub-route. ## RBAC Authorization reuses `rbac.ResourceGroup` with the existing `.InOrganization(...).WithID(...)` scoping model. The `dbauthz` wrappers load the parent `groups` row and authorize against it. No new resource type is introduced. As a result, anyone with `group:update` permissions (Owner, OrgAdmin, or UserAdmin within the organization) can manage AI budgets for that group. ## Read access for group members `database.Group.RBACObject()` grants `policy.ActionRead` to all members of the group through the group ACL: ```go func (g Group) RBACObject() rbac.Object { return rbac.ResourceGroup.WithID(g.ID). InOrg(g.OrganizationID). // Group members can read the group. WithGroupACL(map[string][]policy.Action{ g.ID.String(): { policy.ActionRead, }, }) } ``` Because the `GET` endpoint authorizes against the same loaded `Group` object, any group member can call: ```text GET /api/v2/groups/{group}/ai/budget ``` `PUT` and `DELETE` remain admin-only. The group ACL grants only `ActionRead`, so write operations continue to require role-based `group:update` permissions. ## Alternative considered A dedicated `rbac.ResourceGroupAiBudget` resource would allow budget management to be separated from general group administration. We decided not to add that complexity for now.
This commit is contained in:
Generated
+136
@@ -2520,6 +2520,113 @@ const docTemplate = `{
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/groups/{group}/ai/budget": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Get group AI budget",
|
||||
"operationId": "get-group-ai-budget",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Group ID",
|
||||
"name": "group",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.GroupAIBudget"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Upsert group AI budget",
|
||||
"operationId": "upsert-group-ai-budget",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Group ID",
|
||||
"name": "group",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Upsert group AI budget request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UpsertGroupAIBudgetRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.GroupAIBudget"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Delete group AI budget",
|
||||
"operationId": "delete-group-ai-budget",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Group ID",
|
||||
"name": "group",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/groups/{group}/members": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -18569,6 +18676,26 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.GroupAIBudget": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"group_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"spend_limit_micros": {
|
||||
"type": "integer"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.GroupMembersResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -23753,6 +23880,15 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpsertGroupAIBudgetRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"spend_limit_micros": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpsertWorkspaceAgentPortShareRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Generated
+124
@@ -2217,6 +2217,101 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/groups/{group}/ai/budget": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Get group AI budget",
|
||||
"operationId": "get-group-ai-budget",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Group ID",
|
||||
"name": "group",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.GroupAIBudget"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"consumes": ["application/json"],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Upsert group AI budget",
|
||||
"operationId": "upsert-group-ai-budget",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Group ID",
|
||||
"name": "group",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"description": "Upsert group AI budget request",
|
||||
"name": "request",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.UpsertGroupAIBudgetRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.GroupAIBudget"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Delete group AI budget",
|
||||
"operationId": "delete-group-ai-budget",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Group ID",
|
||||
"name": "group",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "No Content"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/groups/{group}/members": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
@@ -16914,6 +17009,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.GroupAIBudget": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"group_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"spend_limit_micros": {
|
||||
"type": "integer"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.GroupMembersResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -21867,6 +21982,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpsertGroupAIBudgetRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"spend_limit_micros": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UpsertWorkspaceAgentPortShareRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -27,6 +27,7 @@ const (
|
||||
CheckUsersServiceAccountLoginType CheckConstraint = "users_service_account_login_type" // users
|
||||
CheckUsersUsernameMinLength CheckConstraint = "users_username_min_length" // users
|
||||
CheckOrganizationIDNotZero CheckConstraint = "organization_id_not_zero" // custom_roles
|
||||
CheckGroupAiBudgetsSpendLimitMicrosCheck CheckConstraint = "group_ai_budgets_spend_limit_micros_check" // group_ai_budgets
|
||||
CheckGroupsChatSpendLimitMicrosCheck CheckConstraint = "groups_chat_spend_limit_micros_check" // groups
|
||||
CheckMcpServerConfigsAuthTypeCheck CheckConstraint = "mcp_server_configs_auth_type_check" // mcp_server_configs
|
||||
CheckMcpServerConfigsAvailabilityCheck CheckConstraint = "mcp_server_configs_availability_check" // mcp_server_configs
|
||||
|
||||
@@ -1418,6 +1418,15 @@ func flattenAndSum(sums map[string]int64, prefix string, m map[string]json.RawMe
|
||||
}
|
||||
}
|
||||
|
||||
func GroupAIBudget(b database.GroupAiBudget) codersdk.GroupAIBudget {
|
||||
return codersdk.GroupAIBudget{
|
||||
GroupID: b.GroupID,
|
||||
SpendLimitMicros: b.SpendLimitMicros,
|
||||
CreatedAt: b.CreatedAt,
|
||||
UpdatedAt: b.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func InvalidatedPresets(invalidatedPresets []database.UpdatePresetsLastInvalidatedAtRow) []codersdk.InvalidatedPreset {
|
||||
var presets []codersdk.InvalidatedPreset
|
||||
for _, p := range invalidatedPresets {
|
||||
|
||||
@@ -2016,6 +2016,18 @@ func (q *querier) DeleteExternalAuthLink(ctx context.Context, arg database.Delet
|
||||
}, q.db.DeleteExternalAuthLink)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteGroupAIBudget(ctx context.Context, groupID uuid.UUID) (database.GroupAiBudget, error) {
|
||||
// Removing a group's AI budget counts as updating the group.
|
||||
group, err := q.db.GetGroupByID(ctx, groupID)
|
||||
if err != nil {
|
||||
return database.GroupAiBudget{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, group); err != nil {
|
||||
return database.GroupAiBudget{}, err
|
||||
}
|
||||
return q.db.DeleteGroupAIBudget(ctx, groupID)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteGroupByID(ctx context.Context, id uuid.UUID) error {
|
||||
return deleteQ(q.log, q.auth, q.db.GetGroupByID, q.db.DeleteGroupByID)(ctx, id)
|
||||
}
|
||||
@@ -3364,6 +3376,18 @@ func (q *querier) GetGitSSHKey(ctx context.Context, userID uuid.UUID) (database.
|
||||
return fetchWithAction(q.log, q.auth, policy.ActionReadPersonal, q.db.GetGitSSHKey)(ctx, userID)
|
||||
}
|
||||
|
||||
func (q *querier) GetGroupAIBudget(ctx context.Context, groupID uuid.UUID) (database.GroupAiBudget, error) {
|
||||
// Reading a group's AI budget requires read on the parent group.
|
||||
group, err := q.db.GetGroupByID(ctx, groupID)
|
||||
if err != nil {
|
||||
return database.GroupAiBudget{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, group); err != nil {
|
||||
return database.GroupAiBudget{}, err
|
||||
}
|
||||
return q.db.GetGroupAIBudget(ctx, groupID)
|
||||
}
|
||||
|
||||
func (q *querier) GetGroupByID(ctx context.Context, id uuid.UUID) (database.Group, error) {
|
||||
return fetch(q.log, q.auth, q.db.GetGroupByID)(ctx, id)
|
||||
}
|
||||
@@ -7927,6 +7951,18 @@ func (q *querier) UpsertDefaultProxy(ctx context.Context, arg database.UpsertDef
|
||||
return q.db.UpsertDefaultProxy(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpsertGroupAIBudget(ctx context.Context, arg database.UpsertGroupAIBudgetParams) (database.GroupAiBudget, error) {
|
||||
// Setting a group's AI budget counts as updating the group.
|
||||
group, err := q.db.GetGroupByID(ctx, arg.GroupID)
|
||||
if err != nil {
|
||||
return database.GroupAiBudget{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, group); err != nil {
|
||||
return database.GroupAiBudget{}, err
|
||||
}
|
||||
return q.db.UpsertGroupAIBudget(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpsertHealthSettings(ctx context.Context, value string) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
|
||||
return err
|
||||
|
||||
@@ -6209,6 +6209,31 @@ func (s *MethodTestSuite) TestAIBridge() {
|
||||
check.Args(database.GetAIModelPriceByProviderModelParams{}).Asserts(rbac.ResourceAiModelPrice, policy.ActionRead)
|
||||
}))
|
||||
|
||||
s.Run("GetGroupAIBudget", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
g := testutil.Fake(s.T(), faker, database.Group{})
|
||||
b := testutil.Fake(s.T(), faker, database.GroupAiBudget{GroupID: g.ID})
|
||||
dbm.EXPECT().GetGroupByID(gomock.Any(), g.ID).Return(g, nil).AnyTimes()
|
||||
dbm.EXPECT().GetGroupAIBudget(gomock.Any(), g.ID).Return(b, nil).AnyTimes()
|
||||
check.Args(g.ID).Asserts(g, policy.ActionRead).Returns(b)
|
||||
}))
|
||||
|
||||
s.Run("UpsertGroupAIBudget", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
g := testutil.Fake(s.T(), faker, database.Group{})
|
||||
b := testutil.Fake(s.T(), faker, database.GroupAiBudget{GroupID: g.ID})
|
||||
arg := database.UpsertGroupAIBudgetParams{GroupID: g.ID, SpendLimitMicros: b.SpendLimitMicros}
|
||||
dbm.EXPECT().GetGroupByID(gomock.Any(), g.ID).Return(g, nil).AnyTimes()
|
||||
dbm.EXPECT().UpsertGroupAIBudget(gomock.Any(), arg).Return(b, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(g, policy.ActionUpdate).Returns(b)
|
||||
}))
|
||||
|
||||
s.Run("DeleteGroupAIBudget", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
g := testutil.Fake(s.T(), faker, database.Group{})
|
||||
b := testutil.Fake(s.T(), faker, database.GroupAiBudget{GroupID: g.ID})
|
||||
dbm.EXPECT().GetGroupByID(gomock.Any(), g.ID).Return(g, nil).AnyTimes()
|
||||
dbm.EXPECT().DeleteGroupAIBudget(gomock.Any(), g.ID).Return(b, nil).AnyTimes()
|
||||
check.Args(g.ID).Asserts(g, policy.ActionUpdate).Returns(b)
|
||||
}))
|
||||
|
||||
s.Run("GetAIProviderByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
provider := testutil.Fake(s.T(), faker, database.AIProvider{})
|
||||
dbm.EXPECT().GetAIProviderByID(gomock.Any(), provider.ID).Return(provider, nil).AnyTimes()
|
||||
|
||||
@@ -537,6 +537,14 @@ func (m queryMetricsStore) DeleteExternalAuthLink(ctx context.Context, arg datab
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteGroupAIBudget(ctx context.Context, groupID uuid.UUID) (database.GroupAiBudget, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.DeleteGroupAIBudget(ctx, groupID)
|
||||
m.queryLatencies.WithLabelValues("DeleteGroupAIBudget").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteGroupAIBudget").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteGroupByID(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteGroupByID(ctx, id)
|
||||
@@ -1857,6 +1865,14 @@ func (m queryMetricsStore) GetGitSSHKey(ctx context.Context, userID uuid.UUID) (
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetGroupAIBudget(ctx context.Context, groupID uuid.UUID) (database.GroupAiBudget, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetGroupAIBudget(ctx, groupID)
|
||||
m.queryLatencies.WithLabelValues("GetGroupAIBudget").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetGroupAIBudget").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetGroupByID(ctx context.Context, id uuid.UUID) (database.Group, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetGroupByID(ctx, id)
|
||||
@@ -5761,6 +5777,14 @@ func (m queryMetricsStore) UpsertDefaultProxy(ctx context.Context, arg database.
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpsertGroupAIBudget(ctx context.Context, arg database.UpsertGroupAIBudgetParams) (database.GroupAiBudget, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpsertGroupAIBudget(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpsertGroupAIBudget").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpsertGroupAIBudget").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpsertHealthSettings(ctx context.Context, value string) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpsertHealthSettings(ctx, value)
|
||||
|
||||
@@ -888,6 +888,21 @@ func (mr *MockStoreMockRecorder) DeleteExternalAuthLink(ctx, arg any) *gomock.Ca
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteExternalAuthLink", reflect.TypeOf((*MockStore)(nil).DeleteExternalAuthLink), ctx, arg)
|
||||
}
|
||||
|
||||
// DeleteGroupAIBudget mocks base method.
|
||||
func (m *MockStore) DeleteGroupAIBudget(ctx context.Context, groupID uuid.UUID) (database.GroupAiBudget, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteGroupAIBudget", ctx, groupID)
|
||||
ret0, _ := ret[0].(database.GroupAiBudget)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// DeleteGroupAIBudget indicates an expected call of DeleteGroupAIBudget.
|
||||
func (mr *MockStoreMockRecorder) DeleteGroupAIBudget(ctx, groupID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteGroupAIBudget", reflect.TypeOf((*MockStore)(nil).DeleteGroupAIBudget), ctx, groupID)
|
||||
}
|
||||
|
||||
// DeleteGroupByID mocks base method.
|
||||
func (m *MockStore) DeleteGroupByID(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -3436,6 +3451,21 @@ func (mr *MockStoreMockRecorder) GetGitSSHKey(ctx, userID any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGitSSHKey", reflect.TypeOf((*MockStore)(nil).GetGitSSHKey), ctx, userID)
|
||||
}
|
||||
|
||||
// GetGroupAIBudget mocks base method.
|
||||
func (m *MockStore) GetGroupAIBudget(ctx context.Context, groupID uuid.UUID) (database.GroupAiBudget, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetGroupAIBudget", ctx, groupID)
|
||||
ret0, _ := ret[0].(database.GroupAiBudget)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetGroupAIBudget indicates an expected call of GetGroupAIBudget.
|
||||
func (mr *MockStoreMockRecorder) GetGroupAIBudget(ctx, groupID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetGroupAIBudget", reflect.TypeOf((*MockStore)(nil).GetGroupAIBudget), ctx, groupID)
|
||||
}
|
||||
|
||||
// GetGroupByID mocks base method.
|
||||
func (m *MockStore) GetGroupByID(ctx context.Context, id uuid.UUID) (database.Group, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -10804,6 +10834,21 @@ func (mr *MockStoreMockRecorder) UpsertDefaultProxy(ctx, arg any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertDefaultProxy", reflect.TypeOf((*MockStore)(nil).UpsertDefaultProxy), ctx, arg)
|
||||
}
|
||||
|
||||
// UpsertGroupAIBudget mocks base method.
|
||||
func (m *MockStore) UpsertGroupAIBudget(ctx context.Context, arg database.UpsertGroupAIBudgetParams) (database.GroupAiBudget, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpsertGroupAIBudget", ctx, arg)
|
||||
ret0, _ := ret[0].(database.GroupAiBudget)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpsertGroupAIBudget indicates an expected call of UpsertGroupAIBudget.
|
||||
func (mr *MockStoreMockRecorder) UpsertGroupAIBudget(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertGroupAIBudget", reflect.TypeOf((*MockStore)(nil).UpsertGroupAIBudget), ctx, arg)
|
||||
}
|
||||
|
||||
// UpsertHealthSettings mocks base method.
|
||||
func (m *MockStore) UpsertHealthSettings(ctx context.Context, value string) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+16
@@ -1765,6 +1765,16 @@ CREATE TABLE gitsshkeys (
|
||||
public_key text NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE group_ai_budgets (
|
||||
group_id uuid NOT NULL,
|
||||
spend_limit_micros bigint NOT NULL,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT group_ai_budgets_spend_limit_micros_check CHECK ((spend_limit_micros >= 0))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE group_ai_budgets IS 'Per-group AI spend limit applied to each member of the group. No row means no budget is enforced.';
|
||||
|
||||
CREATE TABLE group_members (
|
||||
user_id uuid NOT NULL,
|
||||
group_id uuid NOT NULL
|
||||
@@ -3594,6 +3604,9 @@ ALTER TABLE ONLY external_auth_links
|
||||
ALTER TABLE ONLY gitsshkeys
|
||||
ADD CONSTRAINT gitsshkeys_pkey PRIMARY KEY (user_id);
|
||||
|
||||
ALTER TABLE ONLY group_ai_budgets
|
||||
ADD CONSTRAINT group_ai_budgets_pkey PRIMARY KEY (group_id);
|
||||
|
||||
ALTER TABLE ONLY group_members
|
||||
ADD CONSTRAINT group_members_user_id_group_id_key UNIQUE (user_id, group_id);
|
||||
|
||||
@@ -4358,6 +4371,9 @@ ALTER TABLE ONLY external_auth_links
|
||||
ALTER TABLE ONLY gitsshkeys
|
||||
ADD CONSTRAINT gitsshkeys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
|
||||
|
||||
ALTER TABLE ONLY group_ai_budgets
|
||||
ADD CONSTRAINT group_ai_budgets_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY group_members
|
||||
ADD CONSTRAINT group_members_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE;
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ const (
|
||||
ForeignKeyGitAuthLinksOauthAccessTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_access_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_access_token_key_id_fkey FOREIGN KEY (oauth_access_token_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||
ForeignKeyGitAuthLinksOauthRefreshTokenKeyID ForeignKeyConstraint = "git_auth_links_oauth_refresh_token_key_id_fkey" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_oauth_refresh_token_key_id_fkey FOREIGN KEY (oauth_refresh_token_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||
ForeignKeyGitSSHKeysUserID ForeignKeyConstraint = "gitsshkeys_user_id_fkey" // ALTER TABLE ONLY gitsshkeys ADD CONSTRAINT gitsshkeys_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
|
||||
ForeignKeyGroupAiBudgetsGroupID ForeignKeyConstraint = "group_ai_budgets_group_id_fkey" // ALTER TABLE ONLY group_ai_budgets ADD CONSTRAINT group_ai_budgets_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE;
|
||||
ForeignKeyGroupMembersGroupID ForeignKeyConstraint = "group_members_group_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_group_id_fkey FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE;
|
||||
ForeignKeyGroupMembersUserID ForeignKeyConstraint = "group_members_user_id_fkey" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ForeignKeyGroupsOrganizationID ForeignKeyConstraint = "groups_organization_id_fkey" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS group_ai_budgets CASCADE;
|
||||
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE group_ai_budgets (
|
||||
group_id UUID PRIMARY KEY REFERENCES groups(id) ON DELETE CASCADE,
|
||||
-- Spend limit applied to each member, in micro-units (1 unit = 1,000,000).
|
||||
spend_limit_micros BIGINT NOT NULL CHECK (spend_limit_micros >= 0),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE group_ai_budgets IS 'Per-group AI spend limit applied to each member of the group. No row means no budget is enforced.';
|
||||
@@ -0,0 +1,5 @@
|
||||
INSERT INTO group_ai_budgets (
|
||||
group_id,
|
||||
spend_limit_micros
|
||||
) VALUES
|
||||
('bb640d07-ca8a-4869-b6bc-ae61ebb2fda1', 500000000);
|
||||
@@ -4819,6 +4819,14 @@ type Group struct {
|
||||
ChatSpendLimitMicros sql.NullInt64 `db:"chat_spend_limit_micros" json:"chat_spend_limit_micros"`
|
||||
}
|
||||
|
||||
// Per-group AI spend limit applied to each member of the group. No row means no budget is enforced.
|
||||
type GroupAiBudget struct {
|
||||
GroupID uuid.UUID `db:"group_id" json:"group_id"`
|
||||
SpendLimitMicros int64 `db:"spend_limit_micros" json:"spend_limit_micros"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type GroupMember struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
UserEmail string `db:"user_email" json:"user_email"`
|
||||
|
||||
@@ -131,6 +131,7 @@ type sqlcQuerier interface {
|
||||
DeleteCustomRole(ctx context.Context, arg DeleteCustomRoleParams) error
|
||||
DeleteExpiredAPIKeys(ctx context.Context, arg DeleteExpiredAPIKeysParams) (int64, error)
|
||||
DeleteExternalAuthLink(ctx context.Context, arg DeleteExternalAuthLinkParams) error
|
||||
DeleteGroupAIBudget(ctx context.Context, groupID uuid.UUID) (GroupAiBudget, error)
|
||||
DeleteGroupByID(ctx context.Context, id uuid.UUID) error
|
||||
DeleteGroupMemberFromGroup(ctx context.Context, arg DeleteGroupMemberFromGroupParams) error
|
||||
DeleteLicense(ctx context.Context, id int32) (int32, error)
|
||||
@@ -457,6 +458,7 @@ type sqlcQuerier interface {
|
||||
GetFilteredInboxNotificationsByUserID(ctx context.Context, arg GetFilteredInboxNotificationsByUserIDParams) ([]InboxNotification, error)
|
||||
GetForcedMCPServerConfigs(ctx context.Context) ([]MCPServerConfig, error)
|
||||
GetGitSSHKey(ctx context.Context, userID uuid.UUID) (GitSSHKey, error)
|
||||
GetGroupAIBudget(ctx context.Context, groupID uuid.UUID) (GroupAiBudget, error)
|
||||
GetGroupByID(ctx context.Context, id uuid.UUID) (Group, error)
|
||||
GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrgAndNameParams) (Group, error)
|
||||
GetGroupMembers(ctx context.Context, includeSystem bool) ([]GroupMember, error)
|
||||
@@ -1334,6 +1336,7 @@ type sqlcQuerier interface {
|
||||
// So we need to store it's configuration here for display purposes.
|
||||
// The functional values are immutable and controlled implicitly.
|
||||
UpsertDefaultProxy(ctx context.Context, arg UpsertDefaultProxyParams) error
|
||||
UpsertGroupAIBudget(ctx context.Context, arg UpsertGroupAIBudgetParams) (GroupAiBudget, error)
|
||||
UpsertHealthSettings(ctx context.Context, value string) error
|
||||
UpsertLastUpdateCheck(ctx context.Context, value string) error
|
||||
UpsertLogoURL(ctx context.Context, value string) error
|
||||
|
||||
@@ -2280,6 +2280,22 @@ func (q *sqlQuerier) UpdateAIBridgeInterceptionEnded(ctx context.Context, arg Up
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteGroupAIBudget = `-- name: DeleteGroupAIBudget :one
|
||||
DELETE FROM group_ai_budgets WHERE group_id = $1 RETURNING group_id, spend_limit_micros, created_at, updated_at
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) DeleteGroupAIBudget(ctx context.Context, groupID uuid.UUID) (GroupAiBudget, error) {
|
||||
row := q.db.QueryRowContext(ctx, deleteGroupAIBudget, groupID)
|
||||
var i GroupAiBudget
|
||||
err := row.Scan(
|
||||
&i.GroupID,
|
||||
&i.SpendLimitMicros,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAIModelPriceByProviderModel = `-- name: GetAIModelPriceByProviderModel :one
|
||||
SELECT provider, model, input_price, output_price, cache_read_price, cache_write_price, created_at, updated_at
|
||||
FROM ai_model_prices
|
||||
@@ -2307,6 +2323,24 @@ func (q *sqlQuerier) GetAIModelPriceByProviderModel(ctx context.Context, arg Get
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getGroupAIBudget = `-- name: GetGroupAIBudget :one
|
||||
SELECT group_id, spend_limit_micros, created_at, updated_at
|
||||
FROM group_ai_budgets
|
||||
WHERE group_id = $1
|
||||
`
|
||||
|
||||
func (q *sqlQuerier) GetGroupAIBudget(ctx context.Context, groupID uuid.UUID) (GroupAiBudget, error) {
|
||||
row := q.db.QueryRowContext(ctx, getGroupAIBudget, groupID)
|
||||
var i GroupAiBudget
|
||||
err := row.Scan(
|
||||
&i.GroupID,
|
||||
&i.SpendLimitMicros,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const upsertAIModelPrices = `-- name: UpsertAIModelPrices :exec
|
||||
INSERT INTO ai_model_prices (
|
||||
provider, model, input_price, output_price, cache_read_price, cache_write_price
|
||||
@@ -2335,6 +2369,32 @@ func (q *sqlQuerier) UpsertAIModelPrices(ctx context.Context, seed json.RawMessa
|
||||
return err
|
||||
}
|
||||
|
||||
const upsertGroupAIBudget = `-- name: UpsertGroupAIBudget :one
|
||||
INSERT INTO group_ai_budgets (group_id, spend_limit_micros)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (group_id) DO UPDATE SET
|
||||
spend_limit_micros = EXCLUDED.spend_limit_micros,
|
||||
updated_at = NOW()
|
||||
RETURNING group_id, spend_limit_micros, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpsertGroupAIBudgetParams struct {
|
||||
GroupID uuid.UUID `db:"group_id" json:"group_id"`
|
||||
SpendLimitMicros int64 `db:"spend_limit_micros" json:"spend_limit_micros"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) UpsertGroupAIBudget(ctx context.Context, arg UpsertGroupAIBudgetParams) (GroupAiBudget, error) {
|
||||
row := q.db.QueryRowContext(ctx, upsertGroupAIBudget, arg.GroupID, arg.SpendLimitMicros)
|
||||
var i GroupAiBudget
|
||||
err := row.Scan(
|
||||
&i.GroupID,
|
||||
&i.SpendLimitMicros,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getActiveAISeatCount = `-- name: GetActiveAISeatCount :one
|
||||
SELECT
|
||||
COUNT(*)
|
||||
|
||||
@@ -24,3 +24,19 @@ ON CONFLICT (provider, model) DO UPDATE SET
|
||||
SELECT *
|
||||
FROM ai_model_prices
|
||||
WHERE provider = @provider AND model = @model;
|
||||
|
||||
-- name: GetGroupAIBudget :one
|
||||
SELECT *
|
||||
FROM group_ai_budgets
|
||||
WHERE group_id = @group_id;
|
||||
|
||||
-- name: UpsertGroupAIBudget :one
|
||||
INSERT INTO group_ai_budgets (group_id, spend_limit_micros)
|
||||
VALUES (@group_id, @spend_limit_micros)
|
||||
ON CONFLICT (group_id) DO UPDATE SET
|
||||
spend_limit_micros = EXCLUDED.spend_limit_micros,
|
||||
updated_at = NOW()
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteGroupAIBudget :one
|
||||
DELETE FROM group_ai_budgets WHERE group_id = @group_id RETURNING *;
|
||||
|
||||
@@ -41,6 +41,7 @@ const (
|
||||
UniqueFilesPkey UniqueConstraint = "files_pkey" // ALTER TABLE ONLY files ADD CONSTRAINT files_pkey PRIMARY KEY (id);
|
||||
UniqueGitAuthLinksProviderIDUserIDKey UniqueConstraint = "git_auth_links_provider_id_user_id_key" // ALTER TABLE ONLY external_auth_links ADD CONSTRAINT git_auth_links_provider_id_user_id_key UNIQUE (provider_id, user_id);
|
||||
UniqueGitSSHKeysPkey UniqueConstraint = "gitsshkeys_pkey" // ALTER TABLE ONLY gitsshkeys ADD CONSTRAINT gitsshkeys_pkey PRIMARY KEY (user_id);
|
||||
UniqueGroupAiBudgetsPkey UniqueConstraint = "group_ai_budgets_pkey" // ALTER TABLE ONLY group_ai_budgets ADD CONSTRAINT group_ai_budgets_pkey PRIMARY KEY (group_id);
|
||||
UniqueGroupMembersUserIDGroupIDKey UniqueConstraint = "group_members_user_id_group_id_key" // ALTER TABLE ONLY group_members ADD CONSTRAINT group_members_user_id_group_id_key UNIQUE (user_id, group_id);
|
||||
UniqueGroupsNameOrganizationIDKey UniqueConstraint = "groups_name_organization_id_key" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_name_organization_id_key UNIQUE (name, organization_id);
|
||||
UniqueGroupsPkey UniqueConstraint = "groups_pkey" // ALTER TABLE ONLY groups ADD CONSTRAINT groups_pkey PRIMARY KEY (id);
|
||||
|
||||
Reference in New Issue
Block a user