feat: add enabled toggle to chat model admin panel (#23665)

Adds an `enabled` toggle to the chat model admin create/edit form so
admins
can disable a model without soft-deleting it. Disabled models stay
visible
in admin settings but stop appearing in user-facing model selectors.

The backend already supported this (`chat_model_configs.enabled` column,
filtered queries, and SDK fields). This change wires it into the admin
UI
and adds coverage on both sides.

**Backend:** three new subtests in `coderd/exp_chats_test.go` verifying
the visibility contract (admin sees disabled models, non-admin doesn't,
update-to-disabled preserves the record).

**Frontend:** `enabled` field added to form logic and seeded from the
existing model (defaults to `true` for new models). A Switch+Tooltip
control renders in the form header, matching the MCP Server panel
pattern.
Two interaction stories cover the create-disabled and toggle-existing
flows.
This commit is contained in:
Michael Suchacz
2026-03-26 17:07:20 +01:00
committed by GitHub
parent 5c7057a67f
commit e937f89081
6 changed files with 321 additions and 12 deletions
+163
View File
@@ -1226,6 +1226,73 @@ func TestListChatModelConfigs(t *testing.T) {
require.True(t, found)
})
t.Run("AdminIncludesDisabledModelConfigs", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
client := newChatClient(t)
_ = coderdtest.CreateFirstUser(t, client.Client)
_, err := client.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "test-api-key",
})
require.NoError(t, err)
contextLimit := int64(4096)
enabled := false
disabledConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
Provider: "openai",
Model: "gpt-4o-disabled",
DisplayName: "GPT-4o Disabled",
Enabled: &enabled,
ContextLimit: &contextLimit,
})
require.NoError(t, err)
require.False(t, disabledConfig.Enabled)
configs, err := client.ListChatModelConfigs(ctx)
require.NoError(t, err)
found := false
for _, config := range configs {
if config.ID == disabledConfig.ID {
found = true
require.False(t, config.Enabled)
require.Equal(t, disabledConfig.DisplayName, config.DisplayName)
}
}
require.True(t, found)
})
t.Run("NonAdminExcludesDisabledModelConfigs", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
adminClient := newChatClient(t)
firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
enabledConfig := createChatModelConfig(t, adminClient)
memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
memberClient := codersdk.NewExperimentalClient(memberClientRaw)
contextLimit := int64(4096)
enabled := false
_, err := adminClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
Provider: "openai",
Model: "gpt-4o-disabled",
DisplayName: "GPT-4o Disabled",
Enabled: &enabled,
ContextLimit: &contextLimit,
})
require.NoError(t, err)
configs, err := memberClient.ListChatModelConfigs(ctx)
require.NoError(t, err)
require.Len(t, configs, 1)
require.Equal(t, enabledConfig.ID, configs[0].ID)
require.True(t, configs[0].Enabled)
})
t.Run("DeserializesLegacyPricingJSON", func(t *testing.T) {
t.Parallel()
@@ -1469,6 +1536,102 @@ func TestUpdateChatModelConfig(t *testing.T) {
requireChatModelPricing(t, configs[0].ModelConfig, pricing)
})
t.Run("DisablePreservesRecordAndHidesItFromNonAdmins", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
adminClient := newChatClient(t)
firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
memberClient := codersdk.NewExperimentalClient(memberClientRaw)
modelConfig := createChatModelConfig(t, adminClient)
enabled := false
updated, err := adminClient.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
Enabled: &enabled,
})
require.NoError(t, err)
require.Equal(t, modelConfig.ID, updated.ID)
require.False(t, updated.Enabled)
adminConfigs, err := adminClient.ListChatModelConfigs(ctx)
require.NoError(t, err)
foundForAdmin := false
for _, config := range adminConfigs {
if config.ID == modelConfig.ID {
foundForAdmin = true
require.False(t, config.Enabled)
}
}
require.True(t, foundForAdmin)
memberConfigs, err := memberClient.ListChatModelConfigs(ctx)
require.NoError(t, err)
for _, config := range memberConfigs {
require.NotEqual(t, modelConfig.ID, config.ID)
}
})
t.Run("ReEnableRestoresVisibilityForNonAdmins", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
adminClient := newChatClient(t)
firstUser := coderdtest.CreateFirstUser(t, adminClient.Client)
memberClientRaw, _ := coderdtest.CreateAnotherUser(t, adminClient.Client, firstUser.OrganizationID)
memberClient := codersdk.NewExperimentalClient(memberClientRaw)
_, err := adminClient.CreateChatProvider(ctx, codersdk.CreateChatProviderConfigRequest{
Provider: "openai",
APIKey: "test-api-key",
})
require.NoError(t, err)
contextLimit := int64(4096)
enabled := false
modelConfig, err := adminClient.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{
Provider: "openai",
Model: "gpt-4o-reenable",
DisplayName: "GPT-4o Re-enable",
Enabled: &enabled,
ContextLimit: &contextLimit,
})
require.NoError(t, err)
require.False(t, modelConfig.Enabled)
memberConfigs, err := memberClient.ListChatModelConfigs(ctx)
require.NoError(t, err)
foundForMember := false
for _, config := range memberConfigs {
if config.ID == modelConfig.ID {
foundForMember = true
}
}
require.False(t, foundForMember)
enabled = true
updated, err := adminClient.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{
Enabled: &enabled,
})
require.NoError(t, err)
require.Equal(t, modelConfig.ID, updated.ID)
require.True(t, updated.Enabled)
memberConfigs, err = memberClient.ListChatModelConfigs(ctx)
require.NoError(t, err)
foundForMember = false
for _, config := range memberConfigs {
if config.ID == modelConfig.ID {
foundForMember = true
require.True(t, config.Enabled)
}
}
require.True(t, foundForMember)
})
t.Run("RejectsNegativePricing", func(t *testing.T) {
t.Parallel()