diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 1a856f59e30..388c35f1144 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -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" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 3b93ddb903f..4553c63fc92 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -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" diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 51ba3c6a8dd..5d6c624d200 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -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, diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 436e50ae24f..f8295c7d1ba 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -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 diff --git a/enterprise/coderd/aibridge_test.go b/enterprise/coderd/aibridge_test.go index 44ace6025c8..00c7b4dde7a 100644 --- a/enterprise/coderd/aibridge_test.go +++ b/enterprise/coderd/aibridge_test.go @@ -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{ diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index bff2523fdb1..8df7476400e 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 b16a2920fd9..5dadf27b2b2 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -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", diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.stories.tsx index 1e0900dc254..6260b96dc71 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"; @@ -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 = { @@ -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(); }); diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx index 828ffcc17c9..2cc33a8fa03 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 b117e49af78..fc92df4089c 100644 --- a/site/src/pages/GroupsPage/GroupPage.stories.tsx +++ b/site/src/pages/GroupsPage/GroupPage.stories.tsx @@ -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({ 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),