mirror of
https://github.com/coder/coder.git
synced 2026-09-21 12:44:32 +08:00
chore: add enterprise feature for aibridge (#19976)
Adds enterprise feature "aibridge" and gates the aibridge CRUD and LLM API endpoints behind it.
This commit is contained in:
@@ -90,6 +90,7 @@ const (
|
||||
// enterprise/coderd/license/license.go for the license format.
|
||||
FeatureManagedAgentLimit FeatureName = "managed_agent_limit"
|
||||
FeatureWorkspaceExternalAgent FeatureName = "workspace_external_agent"
|
||||
FeatureAIBridge FeatureName = "aibridge"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -117,6 +118,7 @@ var (
|
||||
FeatureWorkspacePrebuilds,
|
||||
FeatureManagedAgentLimit,
|
||||
FeatureWorkspaceExternalAgent,
|
||||
FeatureAIBridge,
|
||||
}
|
||||
|
||||
// FeatureNamesMap is a map of all feature names for quick lookups.
|
||||
@@ -136,6 +138,8 @@ func (n FeatureName) Humanize() string {
|
||||
return "Template RBAC"
|
||||
case FeatureSCIM:
|
||||
return "SCIM"
|
||||
case FeatureAIBridge:
|
||||
return "AI Bridge"
|
||||
default:
|
||||
return strings.Title(strings.ReplaceAll(string(n), "_", " "))
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
@@ -31,6 +32,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
now := dbtime.Now()
|
||||
@@ -77,6 +83,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
@@ -158,6 +169,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
|
||||
|
||||
@@ -148,8 +148,12 @@ func (r *RootCmd) Server(_ func()) *serpent.Command {
|
||||
|
||||
experiments := agplcoderd.ReadExperiments(options.Logger, options.DeploymentValues.Experiments.Value())
|
||||
|
||||
var aibridgeDaemon *aibridged.Server
|
||||
// In-memory aibridge daemon.
|
||||
// TODO(@deansheather): the lifecycle of the aibridged server is
|
||||
// probably better managed by the enterprise API type itself. Managing
|
||||
// it in the API type means we can avoid starting it up when the license
|
||||
// is not entitled to the feature.
|
||||
var aibridgeDaemon *aibridged.Server
|
||||
if options.DeploymentValues.AI.BridgeConfig.Enabled {
|
||||
if experiments.Enabled(codersdk.ExperimentAIBridge) {
|
||||
aibridgeDaemon, err = newAIBridgeDaemon(api)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package coderd_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -15,12 +16,37 @@ import (
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/cryptorand"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/license"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestAIBridgeListInterceptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("RequiresLicenseFeature", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
dv.Experiments = []string{string(codersdk.ExperimentAIBridge)}
|
||||
client, _ := coderdenttest.New(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
// No aibridge feature
|
||||
Features: license.Features{},
|
||||
},
|
||||
})
|
||||
experimentalClient := codersdk.NewExperimentalClient(client)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
_, err := experimentalClient.AIBridgeListInterceptions(ctx, codersdk.AIBridgeListInterceptionsFilter{})
|
||||
var sdkErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &sdkErr)
|
||||
require.Equal(t, http.StatusForbidden, sdkErr.StatusCode())
|
||||
require.Equal(t, "AI Bridge is a Premium feature. Contact sales!", sdkErr.Message)
|
||||
})
|
||||
|
||||
t.Run("EmptyDB", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
dv := coderdtest.DeploymentValues(t)
|
||||
@@ -29,6 +55,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
experimentalClient := codersdk.NewExperimentalClient(client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
@@ -45,6 +76,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
experimentalClient := codersdk.NewExperimentalClient(client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
@@ -126,6 +162,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
experimentalClient := codersdk.NewExperimentalClient(client)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
@@ -210,6 +251,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
adminExperimentalClient := codersdk.NewExperimentalClient(adminClient)
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
@@ -249,6 +295,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
experimentalClient := codersdk.NewExperimentalClient(client)
|
||||
_, secondUser := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID)
|
||||
@@ -407,6 +458,11 @@ func TestAIBridgeListInterceptions(t *testing.T) {
|
||||
Options: &coderdtest.Options{
|
||||
DeploymentValues: dv,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
codersdk.FeatureAIBridge: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
experimentalClient := codersdk.NewExperimentalClient(client)
|
||||
|
||||
|
||||
@@ -229,6 +229,7 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
|
||||
api.AGPL.ExperimentalHandler.Group(func(r chi.Router) {
|
||||
r.Route("/aibridge", func(r chi.Router) {
|
||||
r.Use(
|
||||
api.RequireFeatureMW(codersdk.FeatureAIBridge),
|
||||
httpmw.RequireExperimentWithDevBypass(api.AGPL.Experiments, codersdk.ExperimentAIBridge),
|
||||
)
|
||||
r.Group(func(r chi.Router) {
|
||||
@@ -770,6 +771,7 @@ func (api *API) updateEntitlements(ctx context.Context) error {
|
||||
codersdk.FeatureUserRoleManagement: true,
|
||||
codersdk.FeatureAccessControl: true,
|
||||
codersdk.FeatureControlSharedPorts: true,
|
||||
codersdk.FeatureAIBridge: true,
|
||||
})
|
||||
if err != nil {
|
||||
return codersdk.Entitlements{}, err
|
||||
|
||||
@@ -890,6 +890,7 @@ func TestLicenseEntitlements(t *testing.T) {
|
||||
codersdk.FeatureAccessControl: true,
|
||||
codersdk.FeatureControlSharedPorts: true,
|
||||
codersdk.FeatureWorkspaceExternalAgent: true,
|
||||
codersdk.FeatureAIBridge: true,
|
||||
}
|
||||
|
||||
legacyLicense := func() *coderdenttest.LicenseOptions {
|
||||
|
||||
Generated
+2
@@ -1222,6 +1222,7 @@ export interface Feature {
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export type FeatureName =
|
||||
| "aibridge"
|
||||
| "access_control"
|
||||
| "advanced_template_scheduling"
|
||||
| "appearance"
|
||||
@@ -1246,6 +1247,7 @@ export type FeatureName =
|
||||
| "workspace_proxy";
|
||||
|
||||
export const FeatureNames: FeatureName[] = [
|
||||
"aibridge",
|
||||
"access_control",
|
||||
"advanced_template_scheduling",
|
||||
"appearance",
|
||||
|
||||
Reference in New Issue
Block a user