From 149f9fef690951004e9915d4e631c879e2442753 Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Tue, 28 Jul 2026 11:14:04 +0000 Subject: [PATCH] chore: remove ai-gateway-cost-control experiment flag The AI Gateway cost control feature is now GA, so remove the ExperimentAIGatewayCostControl experiment and its gating: - Drop the experiment from codersdk/deployment.go (const, DisplayName, ExperimentsKnown). - Remove RequireExperiment gating from the AI cost control API endpoints in enterprise/coderd; these remain gated by FeatureAIBridge. - Remove experiment checks and now-obsolete negative-path tests. - Remove the experiment gate from the cost control frontend UI. - Regenerate API docs and TypeScript types. --- coderd/apidoc/docs.go | 4 - coderd/apidoc/swagger.json | 4 - codersdk/deployment.go | 4 - docs/reference/api/schemas.md | 6 +- enterprise/coderd/aibridge_test.go | 136 +----------------- enterprise/coderd/coderd.go | 12 -- site/src/api/typesGenerated.ts | 2 - .../UserDropdown/UserDropdown.stories.tsx | 11 +- .../Navbar/UserDropdown/UserDropdown.tsx | 7 +- site/src/pages/GroupsPage/AIBudgetPeriod.tsx | 7 +- .../src/pages/GroupsPage/GroupMembersPage.tsx | 7 +- .../pages/GroupsPage/GroupPage.stories.tsx | 6 - .../pages/GroupsPage/GroupSettingsPage.tsx | 8 +- site/src/pages/GroupsPage/GroupsPage.tsx | 7 +- 14 files changed, 14 insertions(+), 207 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 6f9843821fe..2c7b45661a0 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -20210,12 +20210,10 @@ const docTemplate = `{ "minimum-implicit-member", "workspace-capable-licensing", "ai-gateway-seat-exclusion", - "ai-gateway-cost-control", "chat-advisor", "chat-virtual-desktop" ], "x-enum-comments": { - "ExperimentAIGatewayCostControl": "Enables AI Gateway cost control functionality.", "ExperimentAIGatewaySeatExclusion": "Excludes AI Gateway (AI Bridge) usage from AI Governance seat consumption.", "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.", "ExperimentChatAdvisor": "Enables the advisor tool for root agent chats.", @@ -20242,7 +20240,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." ], @@ -20258,7 +20255,6 @@ const docTemplate = `{ "ExperimentMinimumImplicitMember", "ExperimentWorkspaceCapableLicensing", "ExperimentAIGatewaySeatExclusion", - "ExperimentAIGatewayCostControl", "ExperimentChatAdvisor", "ExperimentChatVirtualDesktop" ] diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 59bfe973780..2aaac30fc74 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -18354,12 +18354,10 @@ "minimum-implicit-member", "workspace-capable-licensing", "ai-gateway-seat-exclusion", - "ai-gateway-cost-control", "chat-advisor", "chat-virtual-desktop" ], "x-enum-comments": { - "ExperimentAIGatewayCostControl": "Enables AI Gateway cost control functionality.", "ExperimentAIGatewaySeatExclusion": "Excludes AI Gateway (AI Bridge) usage from AI Governance seat consumption.", "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.", "ExperimentChatAdvisor": "Enables the advisor tool for root agent chats.", @@ -18386,7 +18384,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." ], @@ -18402,7 +18399,6 @@ "ExperimentMinimumImplicitMember", "ExperimentWorkspaceCapableLicensing", "ExperimentAIGatewaySeatExclusion", - "ExperimentAIGatewayCostControl", "ExperimentChatAdvisor", "ExperimentChatVirtualDesktop" ] diff --git a/codersdk/deployment.go b/codersdk/deployment.go index f5430fc2fae..c95d910a0ed 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -5261,7 +5261,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. ) @@ -5290,8 +5289,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: @@ -5317,7 +5314,6 @@ var ExperimentsKnown = Experiments{ ExperimentMinimumImplicitMember, ExperimentWorkspaceCapableLicensing, ExperimentAIGatewaySeatExclusion, - ExperimentAIGatewayCostControl, ExperimentChatAdvisor, ExperimentChatVirtualDesktop, } diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 80023c6ccc6..d75daba76f5 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -7242,9 +7242,9 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o #### Enumerated Values -| Value(s) | -|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `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) | +|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `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 diff --git a/enterprise/coderd/aibridge_test.go b/enterprise/coderd/aibridge_test.go index 7410f9279c1..4aa237d5dda 100644 --- a/enterprise/coderd/aibridge_test.go +++ b/enterprise/coderd/aibridge_test.go @@ -2929,7 +2929,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{ @@ -3027,7 +3026,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{ @@ -3117,7 +3115,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{ @@ -3133,28 +3130,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 @@ -3344,7 +3319,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{ @@ -3398,7 +3372,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{ @@ -3417,30 +3390,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() @@ -3641,7 +3590,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{ @@ -3744,22 +3692,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) { @@ -3767,9 +3707,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}, @@ -4412,7 +4349,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{ @@ -4577,7 +4513,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{ @@ -4600,34 +4535,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() @@ -4756,7 +4663,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{ @@ -4827,7 +4733,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{ @@ -4919,7 +4824,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{ @@ -4942,34 +4846,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() @@ -5199,7 +5075,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}, @@ -5258,7 +5133,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}, @@ -5348,7 +5222,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{ @@ -5438,16 +5311,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) } @@ -5499,7 +5370,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{ diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index d65dd1f6950..bf5a1970311 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -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), diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 543a5d9dd86..00ae98a946b 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -4831,7 +4831,6 @@ export const EntitlementsWarningHeader = "X-Coder-Entitlements-Warning"; // From codersdk/deployment.go export type Experiment = - | "ai-gateway-cost-control" | "ai-gateway-seat-exclusion" | "auto-fill-parameters" | "chat-advisor" @@ -4847,7 +4846,6 @@ export type Experiment = | "workspace-usage"; export const Experiments: Experiment[] = [ - "ai-gateway-cost-control", "ai-gateway-seat-exclusion", "auto-fill-parameters", "chat-advisor", diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx index 521c9086c5d..82e926378ce 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx @@ -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"; @@ -20,9 +16,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 = { @@ -58,7 +53,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(); }); diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx index a7470d826ef..a7fbeb74acf 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx @@ -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 = ({ 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, diff --git a/site/src/pages/GroupsPage/AIBudgetPeriod.tsx b/site/src/pages/GroupsPage/AIBudgetPeriod.tsx index 8ce3e8d8e9e..b716da63136 100644 --- a/site/src/pages/GroupsPage/AIBudgetPeriod.tsx +++ b/site/src/pages/GroupsPage/AIBudgetPeriod.tsx @@ -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) { diff --git a/site/src/pages/GroupsPage/GroupMembersPage.tsx b/site/src/pages/GroupsPage/GroupMembersPage.tsx index beda74199d6..ff4ee823f29 100644 --- a/site/src/pages/GroupsPage/GroupMembersPage.tsx +++ b/site/src/pages/GroupsPage/GroupMembersPage.tsx @@ -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(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, diff --git a/site/src/pages/GroupsPage/GroupPage.stories.tsx b/site/src/pages/GroupsPage/GroupPage.stories.tsx index 8b8c15ba3be..c28c1c9785f 100644 --- a/site/src/pages/GroupsPage/GroupPage.stories.tsx +++ b/site/src/pages/GroupsPage/GroupPage.stories.tsx @@ -157,7 +157,6 @@ export const MembersSpendError: Story = { }, parameters: { features: ["aibridge"], - experiments: ["ai-gateway-cost-control"], queries: [ groupQuery(MockGroupWithoutMembers), groupMembersQuery({ users: [MockUserMember], count: 1 }), @@ -312,7 +311,6 @@ const mockGroupBudget: GroupAIBudget = { export const WithMemberAIBudget: Story = { parameters: { features: ["aibridge"], - experiments: ["ai-gateway-cost-control"], queries: [ groupQuery(MockGroupWithoutMembers), groupMembersQuery({ @@ -395,7 +393,6 @@ export const WithoutMemberAIBudgetColumn: Story = { export const AIBudgetActionDisabledForOtherGroup: Story = { parameters: { features: ["aibridge"], - experiments: ["ai-gateway-cost-control"], queries: [ groupQuery(MockGroupWithoutMembers), groupMembersQuery({ @@ -449,7 +446,6 @@ export const AIBudgetActionDisabledForOtherGroup: Story = { export const WithMemberAIBudgetInAnotherOrg: Story = { parameters: { features: ["aibridge"], - experiments: ["ai-gateway-cost-control"], queries: [ groupQuery(MockGroupWithoutMembers), groupMembersQuery({ @@ -506,7 +502,6 @@ export const WithMemberAIBudgetInAnotherOrg: Story = { export const OpenAIBudgetForCurrentGroupMember: Story = { parameters: { features: ["aibridge"], - experiments: ["ai-gateway-cost-control"], queries: [ groupQuery(MockGroupWithoutMembers), groupMembersQuery({ @@ -661,7 +656,6 @@ const showcaseSpends: GroupMemberAISpend[] = [ export const AIBudgetShowcase: Story = { parameters: { features: ["aibridge"], - experiments: ["ai-gateway-cost-control"], queries: [ groupQuery(MockGroupWithoutMembers), groupMembersQuery({ diff --git a/site/src/pages/GroupsPage/GroupSettingsPage.tsx b/site/src/pages/GroupsPage/GroupSettingsPage.tsx index 5e34c186d43..e7f5d29359a 100644 --- a/site/src/pages/GroupsPage/GroupSettingsPage.tsx +++ b/site/src/pages/GroupsPage/GroupSettingsPage.tsx @@ -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, diff --git a/site/src/pages/GroupsPage/GroupsPage.tsx b/site/src/pages/GroupsPage/GroupsPage.tsx index 65d592a9efb..b1fd6d89f50 100644 --- a/site/src/pages/GroupsPage/GroupsPage.tsx +++ b/site/src/pages/GroupsPage/GroupsPage.tsx @@ -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),