mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add group AI spend endpoint (#27568)
## Description
Adds `GET /api/v2/groups/{group}/ai/spend`, returning the AI spend limit
and aggregate spend for a single group over the current budget period.
The period is derived from the deployment's configured budget period
rather than being caller-specified, matching the other AI spend
endpoints.
## Changes
- Add the `groupAISpend` handler and route, gated by the
`aigateway-cost-control` experiment and the `AIBridge` feature.
- Reuse the existing `GetOrganizationGroupsAISpend` query with a single
group ID, so no new query or authorization path is introduced.
- Add the `GroupAISpend` codersdk type and client method.
Closes
https://linear.app/codercom/issue/AIGOV-475/implement-apiv2groupsgroupaispend
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
This commit is contained in:
Generated
+63
@@ -3400,6 +3400,42 @@ const docTemplate = `{
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/groups/{group}/ai/spend": {
|
||||
"get": {
|
||||
"description": "Returns the AI spend limit and aggregate spend for the group.",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Enterprise"
|
||||
],
|
||||
"summary": "Get group AI spend",
|
||||
"operationId": "get-group-ai-spend",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Group ID",
|
||||
"name": "group",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.GroupAISpend"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/groups/{group}/members": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -20642,6 +20678,33 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.GroupAISpend": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"current_spend_micros": {
|
||||
"description": "CurrentSpendMicros is the group's spend over the current budget\nperiod.",
|
||||
"type": "integer"
|
||||
},
|
||||
"group_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"period_end": {
|
||||
"description": "PeriodEnd is the exclusive upper bound of the current budget\nperiod.",
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"period_start": {
|
||||
"description": "PeriodStart is the inclusive lower bound of the current budget\nperiod.",
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"spend_limit_micros": {
|
||||
"description": "SpendLimitMicros is the group's configured AI spend limit. Null when\nthe group has no configured budget.",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.GroupMemberAISpend": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Generated
+59
@@ -2999,6 +2999,38 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/groups/{group}/ai/spend": {
|
||||
"get": {
|
||||
"description": "Returns the AI spend limit and aggregate spend for the group.",
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Enterprise"],
|
||||
"summary": "Get group AI spend",
|
||||
"operationId": "get-group-ai-spend",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "Group ID",
|
||||
"name": "group",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.GroupAISpend"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/groups/{group}/members": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
@@ -18790,6 +18822,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.GroupAISpend": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"current_spend_micros": {
|
||||
"description": "CurrentSpendMicros is the group's spend over the current budget\nperiod.",
|
||||
"type": "integer"
|
||||
},
|
||||
"group_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"period_end": {
|
||||
"description": "PeriodEnd is the exclusive upper bound of the current budget\nperiod.",
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"period_start": {
|
||||
"description": "PeriodStart is the inclusive lower bound of the current budget\nperiod.",
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"spend_limit_micros": {
|
||||
"description": "SpendLimitMicros is the group's configured AI spend limit. Null when\nthe group has no configured budget.",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.GroupMemberAISpend": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -92,6 +92,13 @@ type OrganizationGroupAISpend struct {
|
||||
CurrentSpendMicros int64 `json:"current_spend_micros"`
|
||||
}
|
||||
|
||||
// GroupAISpend is the current AI spend snapshot for a single group within
|
||||
// the active budget period.
|
||||
type GroupAISpend struct {
|
||||
AISpendPeriodWindow
|
||||
OrganizationGroupAISpend
|
||||
}
|
||||
|
||||
// GroupMembersAISpend reports per-member AI spend attributed to a specific
|
||||
// group in the active budget period.
|
||||
type GroupMembersAISpend struct {
|
||||
@@ -577,6 +584,25 @@ func (c *Client) OrganizationGroupsAISpend(ctx context.Context, organization uui
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
// GroupAISpend returns AI spend for the given group within the active budget
|
||||
// period.
|
||||
func (c *Client) GroupAISpend(ctx context.Context, group uuid.UUID) (GroupAISpend, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet,
|
||||
fmt.Sprintf("/api/v2/groups/%s/ai/spend", group.String()),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return GroupAISpend{}, xerrors.Errorf("make request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return GroupAISpend{}, ReadBodyAsError(res)
|
||||
}
|
||||
var resp GroupAISpend
|
||||
return resp, json.NewDecoder(res.Body).Decode(&resp)
|
||||
}
|
||||
|
||||
// GroupMembersAISpend returns AI spend attributed to the given group for the
|
||||
// specified users within the active budget period. At most 100 user IDs may be
|
||||
// requested per call, and callers with more members are expected to batch
|
||||
|
||||
Generated
+43
@@ -1035,6 +1035,49 @@ curl -X DELETE http://coder-server:8080/api/v2/groups/{group}/ai/budget \
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get group AI spend
|
||||
|
||||
### Code samples
|
||||
|
||||
```sh
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/groups/{group}/ai/spend \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /api/v2/groups/{group}/ai/spend`
|
||||
|
||||
Returns the AI spend limit and aggregate spend for the group.
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
|---------|------|--------------|----------|-------------|
|
||||
| `group` | path | string(uuid) | true | Group ID |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"current_spend_micros": 0,
|
||||
"group_id": "306db4e0-7449-4501-b76f-075576fe2d8f",
|
||||
"period_end": "2019-08-24T14:15:22Z",
|
||||
"period_start": "2019-08-24T14:15:22Z",
|
||||
"spend_limit_micros": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
|--------|---------------------------------------------------------|-------------|----------------------------------------------------------|
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.GroupAISpend](schemas.md#codersdkgroupaispend) |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get group members by group ID
|
||||
|
||||
### Code samples
|
||||
|
||||
Generated
+22
@@ -7750,6 +7750,28 @@ Only certain features set these fields: - FeatureManagedAgentLimit|
|
||||
| `spend_limit_micros` | integer | false | | |
|
||||
| `updated_at` | string | false | | |
|
||||
|
||||
## codersdk.GroupAISpend
|
||||
|
||||
```json
|
||||
{
|
||||
"current_spend_micros": 0,
|
||||
"group_id": "306db4e0-7449-4501-b76f-075576fe2d8f",
|
||||
"period_end": "2019-08-24T14:15:22Z",
|
||||
"period_start": "2019-08-24T14:15:22Z",
|
||||
"spend_limit_micros": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|------------------------|---------|----------|--------------|------------------------------------------------------------------------------------------------------------|
|
||||
| `current_spend_micros` | integer | false | | Current spend micros is the group's spend over the current budget period. |
|
||||
| `group_id` | string | false | | |
|
||||
| `period_end` | string | false | | Period end is the exclusive upper bound of the current budget period. |
|
||||
| `period_start` | string | false | | Period start is the inclusive lower bound of the current budget period. |
|
||||
| `spend_limit_micros` | integer | false | | Spend limit micros is the group's configured AI spend limit. Null when the group has no configured budget. |
|
||||
|
||||
## codersdk.GroupMemberAISpend
|
||||
|
||||
```json
|
||||
|
||||
@@ -1232,6 +1232,58 @@ func (api *API) exportOrganizationAISpend(rw http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
}
|
||||
|
||||
// @Summary Get group AI spend
|
||||
// @Description Returns the AI spend limit and aggregate spend for the group.
|
||||
// @ID get-group-ai-spend
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Enterprise
|
||||
// @Param group path string true "Group ID" format(uuid)
|
||||
// @Success 200 {object} codersdk.GroupAISpend
|
||||
// @Router /api/v2/groups/{group}/ai/spend [get]
|
||||
func (api *API) groupAISpend(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
group := httpmw.GroupParam(r)
|
||||
logger := api.Logger.With(slog.F("group_id", group.ID))
|
||||
|
||||
periodWindow, err := api.currentAIBudgetWindow()
|
||||
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),
|
||||
)
|
||||
|
||||
rows, err := api.Database.GetOrganizationGroupsAISpend(ctx, database.GetOrganizationGroupsAISpendParams{
|
||||
OrganizationID: group.OrganizationID,
|
||||
GroupIds: []uuid.UUID{group.ID},
|
||||
PeriodStart: periodWindow.Start,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(ctx, "failed to get group AI spend", slog.Error(err))
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
// Read access was already established when the group was extracted from
|
||||
// the route, so the query only returns no rows when the group was deleted
|
||||
// in between.
|
||||
if len(rows) == 0 {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.GroupAISpend{
|
||||
AISpendPeriodWindow: codersdk.AISpendPeriodWindow{
|
||||
PeriodStart: periodWindow.Start,
|
||||
PeriodEnd: periodWindow.End,
|
||||
},
|
||||
OrganizationGroupAISpend: db2sdk.OrganizationGroupAISpend(rows[0]),
|
||||
})
|
||||
}
|
||||
|
||||
// @Summary Get group members AI spend by organization
|
||||
// @Description Returns aggregate AI spend attributed to the group per requested user.
|
||||
// @Description A maximum of 100 user IDs may be requested per call, and requests with more are rejected, so callers are expected to batch across multiple requests.
|
||||
|
||||
@@ -4570,6 +4570,249 @@ func TestExportOrganizationAISpend(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGroupAISpend(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("RequiresLicenseFeature", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.Experiments = []string{string(codersdk.ExperimentAIGatewayCostControl)}
|
||||
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{DeploymentValues: dv},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
adminClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleUserAdmin())
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
group, err := adminClient.CreateGroup(ctx, owner.OrganizationID, codersdk.CreateGroupRequest{
|
||||
Name: "req-license-feature-spend-group",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = adminClient.GroupAISpend(ctx, group.ID)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
|
||||
require.Contains(t, sdkErr.Message, "AI Gateway is a Premium feature")
|
||||
})
|
||||
|
||||
t.Run("RequiresExperiment", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
|
||||
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{DeploymentValues: dv},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
adminClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleUserAdmin())
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
group, err := adminClient.CreateGroup(ctx, owner.OrganizationID, codersdk.CreateGroupRequest{
|
||||
Name: "req-experiment-spend-group",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = adminClient.GroupAISpend(ctx, group.ID)
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
|
||||
require.Contains(t, sdkErr.Message, "ai-gateway-cost-control")
|
||||
})
|
||||
|
||||
t.Run("MalformedGroupID", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
adminClient, _, _ := setupAICostControlTest(t, aiCostControlTestOptions{GroupName: "malformed-group-id-spend-group"})
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Given: a malformed UUID in the path.
|
||||
// When: querying spend.
|
||||
res, err := adminClient.Request(ctx, http.MethodGet, "/api/v2/groups/not-a-uuid/ai/spend", nil)
|
||||
require.NoError(t, err)
|
||||
defer res.Body.Close()
|
||||
|
||||
// Then: 400.
|
||||
require.Equal(t, http.StatusBadRequest, res.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("UnknownGroup", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
adminClient, _, _ := setupAICostControlTest(t, aiCostControlTestOptions{GroupName: "unknown-group-spend-group"})
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
// Given: a group ID that does not exist.
|
||||
// When: querying spend.
|
||||
_, err := adminClient.GroupAISpend(ctx, uuid.New())
|
||||
|
||||
// Then: request fails with 404.
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
||||
})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setBudget bool
|
||||
spendLimit int64
|
||||
spent int64
|
||||
wantSpendLimit *int64
|
||||
wantCurrentSpend int64
|
||||
}{
|
||||
{
|
||||
name: "NoBudgetNoSpend",
|
||||
},
|
||||
{
|
||||
name: "ZeroLimitBudget",
|
||||
setBudget: true,
|
||||
spendLimit: 0,
|
||||
wantSpendLimit: ptr.Ref(int64(0)),
|
||||
wantCurrentSpend: 0,
|
||||
},
|
||||
{
|
||||
name: "BudgetZeroSpend",
|
||||
setBudget: true,
|
||||
spendLimit: 1_000_000_000,
|
||||
wantSpendLimit: ptr.Ref(int64(1_000_000_000)),
|
||||
wantCurrentSpend: 0,
|
||||
},
|
||||
{
|
||||
name: "BudgetWithSpend",
|
||||
setBudget: true,
|
||||
spendLimit: 1_000_000_000,
|
||||
spent: 250_000_000,
|
||||
wantSpendLimit: ptr.Ref(int64(1_000_000_000)),
|
||||
wantCurrentSpend: 250_000_000,
|
||||
},
|
||||
{
|
||||
name: "SpendExceedsLimit",
|
||||
setBudget: true,
|
||||
spendLimit: 1_000_000_000,
|
||||
spent: 1_500_000_000,
|
||||
wantSpendLimit: ptr.Ref(int64(1_000_000_000)),
|
||||
wantCurrentSpend: 1_500_000_000,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given: an admin, a group, and optionally a budget and seeded spend.
|
||||
clock := quartz.NewMock(t)
|
||||
db, ps := dbtestutil.NewDB(t)
|
||||
adminClient, targetUser, group := setupAICostControlTest(t, aiCostControlTestOptions{
|
||||
GroupName: "group-spend-test-group",
|
||||
Clock: clock,
|
||||
Database: db,
|
||||
Pubsub: ps,
|
||||
})
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
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.setBudget {
|
||||
_, err := adminClient.UpsertGroupAIBudget(ctx, group.ID, codersdk.UpsertGroupAIBudgetRequest{
|
||||
SpendLimitMicros: tt.spendLimit,
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
// When: querying the group's spend.
|
||||
got, err := adminClient.GroupAISpend(ctx, group.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: the response reports the expected budget and spend.
|
||||
require.Equal(t, wantPeriodStart, got.PeriodStart)
|
||||
require.Equal(t, wantPeriodEnd, got.PeriodEnd)
|
||||
require.Equal(t, group.ID, got.GroupID)
|
||||
require.Equal(t, tt.wantSpendLimit, got.SpendLimitMicros)
|
||||
require.Equal(t, tt.wantCurrentSpend, got.CurrentSpendMicros)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupAISpendRoleAccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
|
||||
dv.Experiments = []string{string(codersdk.ExperimentAIGatewayCostControl)}
|
||||
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{DeploymentValues: dv},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureTemplateRBAC: 1,
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
codersdk.FeatureMultipleOrganizations: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
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, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
|
||||
otherOrg := coderdenttest.CreateOrganization(t, ownerClient, coderdenttest.CreateOrganizationOptions{})
|
||||
otherOrgMemberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, otherOrg.ID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
group, err := userAdminClient.CreateGroup(ctx, owner.OrganizationID, codersdk.CreateGroupRequest{
|
||||
Name: "group-spend-role-access-group",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
client *codersdk.Client
|
||||
wantGroup bool
|
||||
}{
|
||||
{name: "Owner", client: ownerClient, wantGroup: true},
|
||||
{name: "UserAdmin", client: userAdminClient, wantGroup: true},
|
||||
{name: "OrgAdmin", client: orgAdminClient, wantGroup: true},
|
||||
{name: "OrgUserAdmin", client: orgUserAdminClient, wantGroup: true},
|
||||
{name: "Member", client: memberClient, wantGroup: true},
|
||||
{name: "OtherOrgMember", client: otherOrgMemberClient, wantGroup: false},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
resp, err := tc.client.GroupAISpend(ctx, group.ID)
|
||||
if !tc.wantGroup {
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, group.ID, resp.GroupID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportOrganizationAISpendRoleAccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -633,6 +633,15 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
)
|
||||
r.Get("/", api.groupMembersAISpend)
|
||||
})
|
||||
r.Route("/ai/spend", 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.
|
||||
httpmw.RequireExperiment(api.AGPL.Experiments, codersdk.ExperimentAIGatewayCostControl),
|
||||
api.RequireFeatureMW(codersdk.FeatureAIBridge),
|
||||
)
|
||||
r.Get("/", api.groupAISpend)
|
||||
})
|
||||
r.Route("/ai/budget", func(r chi.Router) {
|
||||
// AI cost controls are a paid feature (AI Governance add-on).
|
||||
r.Use(api.RequireFeatureMW(codersdk.FeatureAIBridge))
|
||||
|
||||
Generated
+9
@@ -5212,6 +5212,15 @@ export interface GroupAIBudget {
|
||||
readonly updated_at: string;
|
||||
}
|
||||
|
||||
// From codersdk/aibridge.go
|
||||
/**
|
||||
* GroupAISpend is the current AI spend snapshot for a single group within
|
||||
* the active budget period.
|
||||
*/
|
||||
export interface GroupAISpend
|
||||
extends AISpendPeriodWindow,
|
||||
OrganizationGroupAISpend {}
|
||||
|
||||
// From codersdk/groups.go
|
||||
export interface GroupArguments {
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user