chore: remove ai-gateway-cost-control experiment flag (#27579)

## Description

Closes
[AIGOV-443](https://linear.app/codercom/issue/AIGOV-443/remove-ai-gateway-cost-control-experiment-flag-once-feature-is-stable).

The AI Gateway cost control feature is planned for GA on the upcoming
release, so this removes the `ExperimentAIGatewayCostControl` experiment
and all of its gating. The cost control API endpoints remain gated by
the `FeatureAIBridge` license feature (the AI Governance add-on), so
this only drops the experiment layer.

## Changes

- **`codersdk/deployment.go`**: remove the
`ExperimentAIGatewayCostControl` const, its `DisplayName()` case, and
its `ExperimentsKnown` entry.
- **`enterprise/coderd/coderd.go`**: remove the
`httpmw.RequireExperiment(...)` gating from the AI cost control routes.
They keep `RequireFeatureMW(codersdk.FeatureAIBridge)`. Affected
endpoints:
  - `GET /organizations/{organization}/groups/ai/spend`
- `GET
/organizations/{organization}/groups/{groupName}/members/ai/spend`
  - `GET /organizations/{organization}/ai/spend/export`
  - `GET /groups/{group}/members/ai/spend`
  - `GET /groups/{group}/ai/spend`
- `GET/PUT/DELETE /users/{user}/ai/budget/override` and `GET
/users/{user}/ai/spend`
- **`enterprise/coderd/aibridge_test.go`**: drop the experiment from
test setup and remove the now-obsolete `RequiresExperiment`
negative-path tests.
- **Frontend (`site/src/...`)**: remove the `ai-gateway-cost-control`
experiment checks from the cost control UI (Groups pages, user dropdown)
and their stories/mocks. The feature is now driven solely by the
`aibridge` feature visibility.
- **Generated**: regenerated `coderd/apidoc/*`,
`docs/reference/api/schemas.md`, and `site/src/api/typesGenerated.ts`.

## Out of scope

The dogfood `CODER_EXPERIMENTS` config lives in a separate infra repo,
not `coder/coder`. Leaving `ai-gateway-cost-control` there is harmless:
unknown experiment values are logged as `"ignoring unknown experiment"`
at startup and otherwise ignored, so no ordering dependency or breakage.
That cleanup can be a follow-up.

<details>
<summary>Implementation notes</summary>

- Verified how unknown experiments are handled in `coderd/coderd.go`
`ReadExperiments`: unknown values produce a warning log and are inert,
so removing the definition before the dogfood config is updated is safe.
- Noticed the group `ai/budget` routes (`/groups/{group}/ai/budget`)
were already gated only by `FeatureAIBridge`, never by the experiment.
After this change all cost control routes are uniformly feature-gated,
resolving that inconsistency.
- Removed an obsolete `RequiresExperiment` subtest in
`TestUserAISpendStatus` that only asserted a 403 from the experiment
gate; with the gate gone it would no longer be blocked pre-RBAC.

</details>

---

_This PR was created by Coder Agents on behalf of @ssncferreira._
This commit is contained in:
Susana Ferreira
2026-07-29 14:59:58 +01:00
committed by GitHub
parent d6a5c8e9f8
commit 3deecb481e
14 changed files with 14 additions and 207 deletions
-4
View File
@@ -20315,13 +20315,11 @@ const docTemplate = `{
"minimum-implicit-member",
"workspace-capable-licensing",
"ai-gateway-seat-exclusion",
"ai-gateway-cost-control",
"chat-advisor",
"chat-virtual-desktop",
"agent-lifecycle-hooks"
],
"x-enum-comments": {
"ExperimentAIGatewayCostControl": "Enables AI Gateway cost control functionality.",
"ExperimentAIGatewaySeatExclusion": "Excludes AI Gateway (AI Bridge) usage from AI Governance seat consumption.",
"ExperimentAgentLifecycleHooks": "Enables chat lifecycle hook webhooks for agent chats.",
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
@@ -20349,7 +20347,6 @@ const docTemplate = `{
"Allows organizations to deviate from the default organization-member roles, in support of Gateway Accounts.",
"Counts only users holding the workspace-create permission toward the license seat limit.",
"Excludes AI Gateway (AI Bridge) usage from AI Governance seat consumption.",
"Enables AI Gateway cost control functionality.",
"Enables the advisor tool for root agent chats.",
"Enables virtual desktop and computer use provider for agents.",
"Enables chat lifecycle hook webhooks for agent chats."
@@ -20366,7 +20363,6 @@ const docTemplate = `{
"ExperimentMinimumImplicitMember",
"ExperimentWorkspaceCapableLicensing",
"ExperimentAIGatewaySeatExclusion",
"ExperimentAIGatewayCostControl",
"ExperimentChatAdvisor",
"ExperimentChatVirtualDesktop",
"ExperimentAgentLifecycleHooks"
-4
View File
@@ -18449,13 +18449,11 @@
"minimum-implicit-member",
"workspace-capable-licensing",
"ai-gateway-seat-exclusion",
"ai-gateway-cost-control",
"chat-advisor",
"chat-virtual-desktop",
"agent-lifecycle-hooks"
],
"x-enum-comments": {
"ExperimentAIGatewayCostControl": "Enables AI Gateway cost control functionality.",
"ExperimentAIGatewaySeatExclusion": "Excludes AI Gateway (AI Bridge) usage from AI Governance seat consumption.",
"ExperimentAgentLifecycleHooks": "Enables chat lifecycle hook webhooks for agent chats.",
"ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.",
@@ -18483,7 +18481,6 @@
"Allows organizations to deviate from the default organization-member roles, in support of Gateway Accounts.",
"Counts only users holding the workspace-create permission toward the license seat limit.",
"Excludes AI Gateway (AI Bridge) usage from AI Governance seat consumption.",
"Enables AI Gateway cost control functionality.",
"Enables the advisor tool for root agent chats.",
"Enables virtual desktop and computer use provider for agents.",
"Enables chat lifecycle hook webhooks for agent chats."
@@ -18500,7 +18497,6 @@
"ExperimentMinimumImplicitMember",
"ExperimentWorkspaceCapableLicensing",
"ExperimentAIGatewaySeatExclusion",
"ExperimentAIGatewayCostControl",
"ExperimentChatAdvisor",
"ExperimentChatVirtualDesktop",
"ExperimentAgentLifecycleHooks"
-4
View File
@@ -5365,7 +5365,6 @@ const (
ExperimentMinimumImplicitMember Experiment = "minimum-implicit-member" // Allows organizations to deviate from the default organization-member roles, in support of Gateway Accounts.
ExperimentWorkspaceCapableLicensing Experiment = "workspace-capable-licensing" // Counts only users holding the workspace-create permission toward the license seat limit.
ExperimentAIGatewaySeatExclusion Experiment = "ai-gateway-seat-exclusion" // Excludes AI Gateway (AI Bridge) usage from AI Governance seat consumption.
ExperimentAIGatewayCostControl Experiment = "ai-gateway-cost-control" // Enables AI Gateway cost control functionality.
ExperimentChatAdvisor Experiment = "chat-advisor" // Enables the advisor tool for root agent chats.
ExperimentChatVirtualDesktop Experiment = "chat-virtual-desktop" // Enables virtual desktop and computer use provider for agents.
ExperimentAgentLifecycleHooks Experiment = "agent-lifecycle-hooks" // Enables chat lifecycle hook webhooks for agent chats.
@@ -5395,8 +5394,6 @@ func (e Experiment) DisplayName() string {
return "Workspace-Capable Licensing"
case ExperimentAIGatewaySeatExclusion:
return "AI Gateway Seat Exclusion"
case ExperimentAIGatewayCostControl:
return "AI Gateway Cost Control"
case ExperimentChatAdvisor:
return "Chat Advisor"
case ExperimentChatVirtualDesktop:
@@ -5424,7 +5421,6 @@ var ExperimentsKnown = Experiments{
ExperimentMinimumImplicitMember,
ExperimentWorkspaceCapableLicensing,
ExperimentAIGatewaySeatExclusion,
ExperimentAIGatewayCostControl,
ExperimentChatAdvisor,
ExperimentChatVirtualDesktop,
ExperimentAgentLifecycleHooks,
+3 -3
View File
@@ -7473,9 +7473,9 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
#### Enumerated Values
| Value(s) |
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `agent-lifecycle-hooks`, `ai-gateway-cost-control`, `ai-gateway-seat-exclusion`, `auto-fill-parameters`, `chat-advisor`, `chat-virtual-desktop`, `example`, `mcp-server-http`, `minimum-implicit-member`, `nats_pubsub`, `notifications`, `oauth2`, `workspace-build-updates`, `workspace-capable-licensing`, `workspace-usage` |
| Value(s) |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `agent-lifecycle-hooks`, `ai-gateway-seat-exclusion`, `auto-fill-parameters`, `chat-advisor`, `chat-virtual-desktop`, `example`, `mcp-server-http`, `minimum-implicit-member`, `nats_pubsub`, `notifications`, `oauth2`, `workspace-build-updates`, `workspace-capable-licensing`, `workspace-usage` |
## codersdk.ExternalAPIKeyScopes
+3 -133
View File
@@ -2998,7 +2998,6 @@ func TestUserAIBudgetOverrideRoleAccess(t *testing.T) {
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{
@@ -3096,7 +3095,6 @@ func TestUserAIBudgetOverrideDeletedOnMembershipRemoval(t *testing.T) {
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{
@@ -3186,7 +3184,6 @@ func TestUserAISpendStatus(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{
@@ -3202,28 +3199,6 @@ func TestUserAISpendStatus(t *testing.T) {
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
@@ -3421,7 +3396,6 @@ func TestUserAISpendStatusRoleAccess(t *testing.T) {
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{
@@ -3475,7 +3449,6 @@ func TestOrganizationGroupsAISpend(t *testing.T) {
t.Parallel()
dv := coderdtest.DeploymentValues(t)
dv.Experiments = []string{string(codersdk.ExperimentAIGatewayCostControl)}
client, owner := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{DeploymentValues: dv},
LicenseOptions: &coderdenttest.LicenseOptions{
@@ -3494,30 +3467,6 @@ func TestOrganizationGroupsAISpend(t *testing.T) {
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)
client, owner := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{DeploymentValues: dv},
LicenseOptions: &coderdenttest.LicenseOptions{
Features: license.Features{
codersdk.FeatureTemplateRBAC: 1,
codersdk.FeatureAIBridge: 1,
},
},
})
ctx := testutil.Context(t, testutil.WaitLong)
//nolint:gocritic // Owner role is irrelevant here; the request is blocked before RBAC.
_, err := client.OrganizationGroupsAISpend(ctx, owner.OrganizationID, []uuid.UUID{uuid.New()})
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("MissingGroupIDs", func(t *testing.T) {
t.Parallel()
@@ -3754,7 +3703,6 @@ func TestOrganizationGroupsAISpendRoleAccess(t *testing.T) {
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{
@@ -3857,22 +3805,14 @@ func TestExportOrganizationAISpend(t *testing.T) {
cases := []struct {
name string
experiments []string
features license.Features
wantMsgContains string
}{
{
name: "RequiresLicenseFeature",
experiments: []string{string(codersdk.ExperimentAIGatewayCostControl)},
features: license.Features{},
wantMsgContains: "AI Gateway is a Premium feature",
},
{
name: "RequiresExperiment",
experiments: nil,
features: license.Features{codersdk.FeatureAIBridge: 1},
wantMsgContains: "ai-gateway-cost-control",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@@ -3880,9 +3820,6 @@ func TestExportOrganizationAISpend(t *testing.T) {
dv := coderdtest.DeploymentValues(t)
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
if len(tc.experiments) > 0 {
dv.Experiments = tc.experiments
}
client, owner := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{DeploymentValues: dv},
LicenseOptions: &coderdenttest.LicenseOptions{Features: tc.features},
@@ -4525,7 +4462,6 @@ func TestExportOrganizationAISpend(t *testing.T) {
db, ps := dbtestutil.NewDB(t)
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, Database: db, Pubsub: ps, Clock: clock},
LicenseOptions: &coderdenttest.LicenseOptions{
@@ -4690,7 +4626,6 @@ func TestGroupAISpend(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{
@@ -4713,34 +4648,6 @@ func TestGroupAISpend(t *testing.T) {
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()
@@ -4904,7 +4811,6 @@ func TestGroupAISpendRoleAccess(t *testing.T) {
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{
@@ -4975,7 +4881,6 @@ func TestExportOrganizationAISpendRoleAccess(t *testing.T) {
db, ps := dbtestutil.NewDB(t)
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, Database: db, Pubsub: ps, Clock: clock},
LicenseOptions: &coderdenttest.LicenseOptions{
@@ -5067,7 +4972,6 @@ func TestGroupMembersAISpend(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{
@@ -5090,34 +4994,6 @@ func TestGroupMembersAISpend(t *testing.T) {
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-members-group",
})
require.NoError(t, err)
_, err = adminClient.GroupMembersAISpend(ctx, group.ID, []uuid.UUID{uuid.New()})
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("MissingUserIDs", func(t *testing.T) {
t.Parallel()
@@ -5347,7 +5223,6 @@ func TestGroupMembersAISpend(t *testing.T) {
dv := coderdtest.DeploymentValues(t)
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
dv.Experiments = []string{string(codersdk.ExperimentAIGatewayCostControl)}
db, ps := dbtestutil.NewDB(t)
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{DeploymentValues: dv, Database: db, Pubsub: ps},
@@ -5406,7 +5281,6 @@ func TestGroupMembersAISpend(t *testing.T) {
dv := coderdtest.DeploymentValues(t)
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
dv.Experiments = []string{string(codersdk.ExperimentAIGatewayCostControl)}
db, ps := dbtestutil.NewDB(t)
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
Options: &coderdtest.Options{DeploymentValues: dv, Database: db, Pubsub: ps},
@@ -5496,7 +5370,6 @@ func TestGroupMembersAISpendRoleAccess(t *testing.T) {
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{
@@ -5586,16 +5459,14 @@ type aiCostControlTestOptions struct {
Retention *time.Duration
}
// 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.
// setupAICostControlTest builds a deployment with FeatureAIBridge licensed,
// 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)}
if opts.Retention != nil {
dv.AI.BridgeConfig.Retention = serpent.Duration(*opts.Retention)
}
@@ -5647,7 +5518,6 @@ func setupUserAIBudgetOverrideAuditTest(t *testing.T) (database.Store, *codersdk
)
dv := coderdtest.DeploymentValues(t)
dv.AI.BridgeConfig.Enabled = serpent.Bool(true)
dv.Experiments = []string{string(codersdk.ExperimentAIGatewayCostControl)}
ownerClient, owner := coderdenttest.New(t, &coderdenttest.Options{
AuditLogging: true,
Options: &coderdtest.Options{
-12
View File
@@ -506,8 +506,6 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
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.organizationGroupsAISpend)
@@ -522,8 +520,6 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
r.Route("/members/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.groupMembersAISpendByOrganization)
@@ -535,8 +531,6 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
r.Use(
apiKeyMiddleware,
httpmw.ExtractOrganizationParam(api.Database),
// 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("/export", api.exportOrganizationAISpend)
@@ -627,8 +621,6 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
r.Route("/members/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.groupMembersAISpend)
@@ -636,8 +628,6 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
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)
@@ -692,8 +682,6 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
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.
httpmw.RequireExperiment(api.AGPL.Experiments, codersdk.ExperimentAIGatewayCostControl),
api.RequireFeatureMW(codersdk.FeatureAIBridge),
apiKeyMiddleware,
httpmw.ExtractUserParam(options.Database),
-2
View File
@@ -5097,7 +5097,6 @@ export const EntitlementsWarningHeader = "X-Coder-Entitlements-Warning";
// From codersdk/deployment.go
export type Experiment =
| "ai-gateway-cost-control"
| "ai-gateway-seat-exclusion"
| "agent-lifecycle-hooks"
| "auto-fill-parameters"
@@ -5114,7 +5113,6 @@ export type Experiment =
| "workspace-usage";
export const Experiments: Experiment[] = [
"ai-gateway-cost-control",
"ai-gateway-seat-exclusion",
"agent-lifecycle-hooks",
"auto-fill-parameters",
@@ -1,11 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, screen, userEvent, waitFor, within } from "storybook/test";
import { meAISpendKey } from "#/api/queries/users";
import type {
Experiment,
FeatureName,
UserAISpendStatus,
} from "#/api/typesGenerated";
import type { FeatureName, UserAISpendStatus } from "#/api/typesGenerated";
import { MockBuildInfo, MockUserOwner } from "#/testHelpers/entities";
import { withDashboardProvider } from "#/testHelpers/storybook";
import { UserDropdown } from "./UserDropdown";
@@ -22,9 +18,8 @@ const mockAISpend: UserAISpendStatus = {
period_end: "2026-07-01T00:00:00Z",
};
const aiCostControl: { features: FeatureName[]; experiments: Experiment[] } = {
const aiCostControl: { features: FeatureName[] } = {
features: ["aibridge"],
experiments: ["ai-gateway-cost-control"],
};
const meta: Meta<typeof UserDropdown> = {
@@ -60,7 +55,7 @@ const Example: Story = {
queries: [{ key: meAISpendKey, data: mockAISpend }],
},
play: async ({ canvasElement, step }) => {
await step("hides AI spend without cost control", async () => {
await step("hides AI spend without the aibridge feature", async () => {
await openDropdown(canvasElement);
expect(screen.queryByText(/AI spend/i)).not.toBeInTheDocument();
});
@@ -9,7 +9,6 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "#/components/DropdownMenu/DropdownMenu";
import { useDashboard } from "#/modules/dashboard/useDashboard";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
import { getSeverity, type UsageSeverity } from "#/utils/budget";
import { UserDropdownAISpend } from "./UserDropdownAISpend";
@@ -34,11 +33,7 @@ export const UserDropdown: FC<UserDropdownProps> = ({
supportLinks,
onSignOut,
}) => {
const { experiments } = useDashboard();
// TODO(AIGOV-443): drop the experiment gate once cost control is stable.
const aibridgeVisible =
Boolean(useFeatureVisibility().aibridge) &&
experiments.includes("ai-gateway-cost-control");
const aibridgeVisible = Boolean(useFeatureVisibility().aibridge);
const { data, isError } = useQuery({
...meAISpend(),
enabled: aibridgeVisible,
+1 -6
View File
@@ -2,16 +2,11 @@ import dayjs from "dayjs";
import type { FC } from "react";
import { useQuery } from "react-query";
import { meAISpend } from "#/api/queries/users";
import { useDashboard } from "#/modules/dashboard/useDashboard";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
/** The current AI budget window, e.g. "June 1 - July 1, 2026". */
export const AIBudgetPeriod: FC = () => {
const { experiments } = useDashboard();
// TODO(AIGOV-443): drop the experiment gate once cost control is stable.
const visible =
Boolean(useFeatureVisibility().aibridge) &&
experiments.includes("ai-gateway-cost-control");
const visible = Boolean(useFeatureVisibility().aibridge);
const { data: aiSpend } = useQuery({ ...meAISpend(), enabled: visible });
if (!visible || !aiSpend) {
@@ -47,7 +47,6 @@ import {
TableHeader,
TableRow,
} from "#/components/Table/Table";
import { useDashboard } from "#/modules/dashboard/useDashboard";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
import { isEveryoneGroup } from "#/modules/groups";
import { cn } from "#/utils/cn";
@@ -81,11 +80,7 @@ const GroupMembersPage: FC = () => {
const canUpdateGroup = permissions ? permissions.canUpdateGroup : false;
const [budgetUser, setBudgetUser] = useState<MemberWithSpend | null>(null);
const { experiments } = useDashboard();
// TODO(AIGOV-443): drop the experiment gate once cost control is stable.
const aibridgeVisible =
Boolean(useFeatureVisibility().aibridge) &&
experiments.includes("ai-gateway-cost-control");
const aibridgeVisible = Boolean(useFeatureVisibility().aibridge);
const { data: aiSpend } = useQuery({
...meAISpend(),
enabled: aibridgeVisible,
@@ -159,7 +159,6 @@ export const MembersSpendError: Story = {
},
parameters: {
features: ["aibridge"],
experiments: ["ai-gateway-cost-control"],
queries: [
groupQuery(MockGroupWithoutMembers),
groupMembersQuery({ users: [MockUserMember], count: 1 }),
@@ -314,7 +313,6 @@ const mockGroupBudget: GroupAIBudget = {
export const WithMemberAIBudget: Story = {
parameters: {
features: ["aibridge"],
experiments: ["ai-gateway-cost-control"],
queries: [
groupQuery(MockGroupWithoutMembers),
groupMembersQuery({
@@ -397,7 +395,6 @@ export const WithoutMemberAIBudgetColumn: Story = {
export const AIBudgetActionDisabledForOtherGroup: Story = {
parameters: {
features: ["aibridge"],
experiments: ["ai-gateway-cost-control"],
queries: [
groupQuery(MockGroupWithoutMembers),
groupMembersQuery({
@@ -451,7 +448,6 @@ export const AIBudgetActionDisabledForOtherGroup: Story = {
export const WithMemberAIBudgetInAnotherOrg: Story = {
parameters: {
features: ["aibridge"],
experiments: ["ai-gateway-cost-control"],
queries: [
groupQuery(MockGroupWithoutMembers),
groupMembersQuery({
@@ -508,7 +504,6 @@ export const WithMemberAIBudgetInAnotherOrg: Story = {
export const OpenAIBudgetForCurrentGroupMember: Story = {
parameters: {
features: ["aibridge"],
experiments: ["ai-gateway-cost-control"],
queries: [
groupQuery(MockGroupWithoutMembers),
groupMembersQuery({
@@ -663,7 +658,6 @@ const showcaseSpends: GroupMemberAISpend[] = [
export const AIBudgetShowcase: Story = {
parameters: {
features: ["aibridge"],
experiments: ["ai-gateway-cost-control"],
queries: [
groupQuery(MockGroupWithoutMembers),
groupMembersQuery({
@@ -10,7 +10,6 @@ import {
} from "#/api/queries/groups";
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
import { Spinner } from "#/components/Spinner/Spinner";
import { useDashboard } from "#/modules/dashboard/useDashboard";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
import { dollarsToMicros, microsToDollars } from "#/utils/currency";
import type { GroupPageOutletContext } from "./GroupPage";
@@ -29,12 +28,7 @@ const GroupSettingsPage: FC = () => {
const patchGroupMutation = useMutation(patchGroup(queryClient, organization));
const navigate = useNavigate();
const { experiments } = useDashboard();
// TODO(AIGOV-443): remove the ai-gateway-cost-control experiment gate once
// the cost-control feature is stable.
const aibridgeVisible =
Boolean(useFeatureVisibility().aibridge) &&
experiments.includes("ai-gateway-cost-control");
const aibridgeVisible = Boolean(useFeatureVisibility().aibridge);
const budgetQuery = useQuery({
...groupAIBudget(groupData.id),
enabled: aibridgeVisible,
+1 -6
View File
@@ -17,7 +17,6 @@ import {
SettingsHeaderDescription,
SettingsHeaderTitle,
} from "#/components/SettingsHeader/SettingsHeader";
import { useDashboard } from "#/modules/dashboard/useDashboard";
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
import { RequirePermission } from "#/modules/permissions/RequirePermission";
import { pageTitle } from "#/utils/page";
@@ -26,12 +25,8 @@ import { GroupsPageView, joinGroupsSpend } from "./GroupsPageView";
const GroupsPage: FC = () => {
const { template_rbac: groupsEnabled, aibridge } = useFeatureVisibility();
const { experiments } = useDashboard();
const { organization, showOrganizations } = useGroupsSettings();
// TODO(AIGOV-443): remove the ai-gateway-cost-control experiment gate once
// the cost-control feature is stable.
const aibridgeVisible =
Boolean(aibridge) && experiments.includes("ai-gateway-cost-control");
const aibridgeVisible = Boolean(aibridge);
const groupsQuery = useQuery({
...groupsByOrganization(organization?.name ?? ""),
enabled: Boolean(organization),