mirror of
https://github.com/coder/coder.git
synced 2026-10-29 17:44:33 +08:00
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:
@@ -1226,6 +1226,73 @@ func TestListChatModelConfigs(t *testing.T) {
|
|||||||
require.True(t, found)
|
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.Run("DeserializesLegacyPricingJSON", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -1469,6 +1536,102 @@ func TestUpdateChatModelConfig(t *testing.T) {
|
|||||||
requireChatModelPricing(t, configs[0].ModelConfig, pricing)
|
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.Run("RejectsNegativePricing", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
+76
-6
@@ -123,6 +123,7 @@ const setupChatSpies = (state: {
|
|||||||
provider: req.provider,
|
provider: req.provider,
|
||||||
model: req.model,
|
model: req.model,
|
||||||
display_name: req.display_name || req.model,
|
display_name: req.display_name || req.model,
|
||||||
|
enabled: req.enabled ?? true,
|
||||||
context_limit:
|
context_limit:
|
||||||
typeof req.context_limit === "number" &&
|
typeof req.context_limit === "number" &&
|
||||||
Number.isFinite(req.context_limit)
|
Number.isFinite(req.context_limit)
|
||||||
@@ -152,12 +153,29 @@ const setupChatSpies = (state: {
|
|||||||
spyOn(API.experimental, "deleteChatProviderConfig").mockResolvedValue(
|
spyOn(API.experimental, "deleteChatProviderConfig").mockResolvedValue(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
spyOn(API.experimental, "updateChatModelConfig").mockResolvedValue(
|
spyOn(API.experimental, "updateChatModelConfig").mockImplementation(
|
||||||
createModelConfig({
|
async (modelConfigId, req) => {
|
||||||
id: "stub",
|
const idx = state.modelConfigs.findIndex((m) => m.id === modelConfigId);
|
||||||
provider: "stub",
|
if (idx < 0) {
|
||||||
model: "stub",
|
throw new Error("Model config not found.");
|
||||||
}),
|
}
|
||||||
|
|
||||||
|
const current = state.modelConfigs[idx];
|
||||||
|
const updated = createModelConfig({
|
||||||
|
...current,
|
||||||
|
...req,
|
||||||
|
id: current.id,
|
||||||
|
provider: current.provider,
|
||||||
|
model: current.model,
|
||||||
|
updated_at: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
state.modelConfigs = state.modelConfigs.map((modelConfig, i) =>
|
||||||
|
i === idx ? updated : modelConfig,
|
||||||
|
);
|
||||||
|
|
||||||
|
return updated;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -507,6 +525,58 @@ export const SubmitModelConfigExplicitly: Story = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const UpdateModelEnabledToggle: Story = {
|
||||||
|
args: { section: "models" as ChatModelAdminSection },
|
||||||
|
beforeEach: () => {
|
||||||
|
setupChatSpies({
|
||||||
|
providerConfigs: [
|
||||||
|
createProviderConfig({
|
||||||
|
id: "provider-openai",
|
||||||
|
provider: "openai",
|
||||||
|
display_name: "OpenAI",
|
||||||
|
source: "database",
|
||||||
|
has_api_key: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
modelConfigs: [
|
||||||
|
createModelConfig({
|
||||||
|
id: "model-enabled",
|
||||||
|
provider: "openai",
|
||||||
|
model: "gpt-test-enabled",
|
||||||
|
display_name: "GPT Test Enabled",
|
||||||
|
enabled: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
modelCatalog: { providers: [] },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const body = within(canvasElement.ownerDocument.body);
|
||||||
|
|
||||||
|
await userEvent.click(await body.findByText("GPT Test Enabled"));
|
||||||
|
|
||||||
|
const enabledSwitch = await body.findByRole("switch", { name: "Enabled" });
|
||||||
|
await expect(enabledSwitch).toBeChecked();
|
||||||
|
await userEvent.click(enabledSwitch);
|
||||||
|
await expect(enabledSwitch).not.toBeChecked();
|
||||||
|
|
||||||
|
await userEvent.click(body.getByRole("button", { name: "Save" }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(API.experimental.updateChatModelConfig).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
expect(API.experimental.updateChatModelConfig).toHaveBeenCalledWith(
|
||||||
|
"model-enabled",
|
||||||
|
expect.objectContaining({ enabled: false }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const modelRow = await body.findByRole("button", {
|
||||||
|
name: /gpt test enabled/i,
|
||||||
|
});
|
||||||
|
await expect(within(modelRow).getByText("disabled")).toBeVisible();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// ── Per-provider model form stories ────────────────────────────
|
// ── Per-provider model form stories ────────────────────────────
|
||||||
// Each story opens the "Add model" form for a specific provider
|
// Each story opens the "Add model" form for a specific provider
|
||||||
// so you can visually verify the schema-driven fields render.
|
// so you can visually verify the schema-driven fields render.
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "#/components/Select/Select";
|
} from "#/components/Select/Select";
|
||||||
import { Spinner } from "#/components/Spinner/Spinner";
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
|
import { Switch } from "#/components/Switch/Switch";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "#/components/Tooltip/Tooltip";
|
||||||
import type { ProviderState } from "./ChatModelAdminPanel";
|
import type { ProviderState } from "./ChatModelAdminPanel";
|
||||||
import {
|
import {
|
||||||
GeneralModelConfigFields,
|
GeneralModelConfigFields,
|
||||||
@@ -40,6 +46,7 @@ import { ProviderIcon } from "./ProviderIcon";
|
|||||||
const validationSchema = Yup.object({
|
const validationSchema = Yup.object({
|
||||||
model: Yup.string().trim().required("Model ID is required."),
|
model: Yup.string().trim().required("Model ID is required."),
|
||||||
displayName: Yup.string(),
|
displayName: Yup.string(),
|
||||||
|
enabled: Yup.boolean(),
|
||||||
contextLimit: Yup.string()
|
contextLimit: Yup.string()
|
||||||
.required("Context limit is required.")
|
.required("Context limit is required.")
|
||||||
.test(
|
.test(
|
||||||
@@ -93,6 +100,7 @@ export const ModelForm: FC<ModelFormProps> = ({
|
|||||||
onDeleteModel,
|
onDeleteModel,
|
||||||
}) => {
|
}) => {
|
||||||
const isEditing = Boolean(editingModel);
|
const isEditing = Boolean(editingModel);
|
||||||
|
const isDefaultModel = isEditing && editingModel?.is_default === true;
|
||||||
const [showPricing, setShowPricing] = useState(false);
|
const [showPricing, setShowPricing] = useState(false);
|
||||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||||
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
||||||
@@ -135,6 +143,9 @@ export const ModelForm: FC<ModelFormProps> = ({
|
|||||||
...(trimmedDisplayName !== (editingModel.display_name ?? "") && {
|
...(trimmedDisplayName !== (editingModel.display_name ?? "") && {
|
||||||
display_name: trimmedDisplayName,
|
display_name: trimmedDisplayName,
|
||||||
}),
|
}),
|
||||||
|
...(values.enabled !== editingModel.enabled && {
|
||||||
|
enabled: values.enabled,
|
||||||
|
}),
|
||||||
...(parsedContextLimit !== null &&
|
...(parsedContextLimit !== null &&
|
||||||
parsedContextLimit !== editingModel.context_limit && {
|
parsedContextLimit !== editingModel.context_limit && {
|
||||||
context_limit: parsedContextLimit,
|
context_limit: parsedContextLimit,
|
||||||
@@ -158,6 +169,7 @@ export const ModelForm: FC<ModelFormProps> = ({
|
|||||||
const req: TypesGen.CreateChatModelConfigRequest = {
|
const req: TypesGen.CreateChatModelConfigRequest = {
|
||||||
provider: selectedProviderState.provider,
|
provider: selectedProviderState.provider,
|
||||||
model: trimmedModel,
|
model: trimmedModel,
|
||||||
|
enabled: true,
|
||||||
...(parsedContextLimit !== null && {
|
...(parsedContextLimit !== null && {
|
||||||
context_limit: parsedContextLimit,
|
context_limit: parsedContextLimit,
|
||||||
}),
|
}),
|
||||||
@@ -192,6 +204,7 @@ export const ModelForm: FC<ModelFormProps> = ({
|
|||||||
|
|
||||||
const hasFieldErrors =
|
const hasFieldErrors =
|
||||||
Object.keys(modelConfigFormBuildResult.fieldErrors).length > 0;
|
Object.keys(modelConfigFormBuildResult.fieldErrors).length > 0;
|
||||||
|
const defaultModelDisableGuard = isDefaultModel && form.values.enabled;
|
||||||
|
|
||||||
// ── Provider select (shared across all form states) ───────
|
// ── Provider select (shared across all form states) ───────
|
||||||
|
|
||||||
@@ -314,6 +327,29 @@ export const ModelForm: FC<ModelFormProps> = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{editingModel && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className="ml-auto inline-flex">
|
||||||
|
<Switch
|
||||||
|
checked={form.values.enabled}
|
||||||
|
onCheckedChange={(v) => {
|
||||||
|
form.setFieldValue("enabled", v);
|
||||||
|
}}
|
||||||
|
aria-label="Enabled"
|
||||||
|
disabled={isSaving || defaultModelDisableGuard}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">
|
||||||
|
{defaultModelDisableGuard
|
||||||
|
? "Default model cannot be disabled. Remove default status first."
|
||||||
|
: form.values.enabled
|
||||||
|
? "Disable this model. It will be hidden from users."
|
||||||
|
: "Enable this model. It will be visible to users."}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<hr className="my-4 border-0 border-t border-solid border-border" />
|
<hr className="my-4 border-0 border-t border-solid border-border" />
|
||||||
|
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ export const ModelsSection: FC<ModelsSectionProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSetDefault = (modelConfig: TypesGen.ChatModelConfig) => {
|
const handleSetDefault = (modelConfig: TypesGen.ChatModelConfig) => {
|
||||||
if (modelConfig.is_default) return;
|
if (modelConfig.is_default || !modelConfig.enabled) return;
|
||||||
void onUpdateModel(modelConfig.id, { is_default: true });
|
void onUpdateModel(modelConfig.id, { is_default: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -310,7 +310,11 @@ export const ModelsSection: FC<ModelsSectionProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleSetDefault(modelConfig);
|
handleSetDefault(modelConfig);
|
||||||
}}
|
}}
|
||||||
aria-disabled={isUpdating || modelConfig.is_default}
|
aria-disabled={
|
||||||
|
isUpdating ||
|
||||||
|
modelConfig.is_default ||
|
||||||
|
!modelConfig.enabled
|
||||||
|
}
|
||||||
aria-label={
|
aria-label={
|
||||||
modelConfig.is_default
|
modelConfig.is_default
|
||||||
? "Default model"
|
? "Default model"
|
||||||
@@ -320,7 +324,9 @@ export const ModelsSection: FC<ModelsSectionProps> = ({
|
|||||||
"flex shrink-0 items-center justify-center bg-transparent border-0 p-0 transition-colors",
|
"flex shrink-0 items-center justify-center bg-transparent border-0 p-0 transition-colors",
|
||||||
modelConfig.is_default
|
modelConfig.is_default
|
||||||
? "text-content-primary"
|
? "text-content-primary"
|
||||||
: "cursor-pointer text-content-secondary/30 hover:text-content-secondary",
|
: !modelConfig.enabled
|
||||||
|
? "cursor-not-allowed text-content-secondary/30"
|
||||||
|
: "cursor-pointer text-content-secondary/30 hover:text-content-secondary",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<PinIcon
|
<PinIcon
|
||||||
@@ -332,9 +338,11 @@ export const ModelsSection: FC<ModelsSectionProps> = ({
|
|||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="left">
|
<TooltipContent side="left">
|
||||||
{modelConfig.is_default
|
{!modelConfig.enabled
|
||||||
? "Pinned as default for new chats"
|
? "Cannot set a disabled model as default"
|
||||||
: "Pin as default for new chats"}
|
: modelConfig.is_default
|
||||||
|
? "Pinned as default for new chats"
|
||||||
|
: "Pin as default for new chats"}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<ChevronRightIcon className="h-5 w-5 shrink-0 text-content-secondary" />
|
<ChevronRightIcon className="h-5 w-5 shrink-0 text-content-secondary" />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type * as TypesGen from "#/api/typesGenerated";
|
import type * as TypesGen from "#/api/typesGenerated";
|
||||||
import {
|
import {
|
||||||
|
buildInitialModelFormValues,
|
||||||
buildModelConfigFromForm,
|
buildModelConfigFromForm,
|
||||||
emptyModelConfigFormState,
|
emptyModelConfigFormState,
|
||||||
extractModelConfigFormState,
|
extractModelConfigFormState,
|
||||||
@@ -90,6 +91,35 @@ const baseChatModelConfig: TypesGen.ChatModelConfig = {
|
|||||||
updated_at: "2025-01-01T00:00:00Z",
|
updated_at: "2025-01-01T00:00:00Z",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── buildInitialModelFormValues ────────────────────────────────
|
||||||
|
|
||||||
|
describe("buildInitialModelFormValues", () => {
|
||||||
|
it("returns create mode defaults including enabled=true", () => {
|
||||||
|
expect(buildInitialModelFormValues()).toEqual({
|
||||||
|
model: "",
|
||||||
|
displayName: "",
|
||||||
|
enabled: true,
|
||||||
|
contextLimit: "",
|
||||||
|
compressionThreshold: "",
|
||||||
|
isDefault: false,
|
||||||
|
config: emptyModelConfigFormState,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves enabled=true when editing an enabled model", () => {
|
||||||
|
expect(buildInitialModelFormValues(baseChatModelConfig).enabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves enabled=false when editing a disabled model", () => {
|
||||||
|
expect(
|
||||||
|
buildInitialModelFormValues({
|
||||||
|
...baseChatModelConfig,
|
||||||
|
enabled: false,
|
||||||
|
}).enabled,
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ── parsePositiveInteger ───────────────────────────────────────
|
// ── parsePositiveInteger ───────────────────────────────────────
|
||||||
|
|
||||||
describe("parsePositiveInteger", () => {
|
describe("parsePositiveInteger", () => {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export type ModelConfigFormBuildResult = {
|
|||||||
export type ModelFormValues = {
|
export type ModelFormValues = {
|
||||||
model: string;
|
model: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
enabled: boolean;
|
||||||
contextLimit: string;
|
contextLimit: string;
|
||||||
compressionThreshold: string;
|
compressionThreshold: string;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
@@ -224,6 +225,7 @@ export const buildInitialModelFormValues = (
|
|||||||
): ModelFormValues => ({
|
): ModelFormValues => ({
|
||||||
model: editingModel?.model ?? "",
|
model: editingModel?.model ?? "",
|
||||||
displayName: editingModel?.display_name ?? "",
|
displayName: editingModel?.display_name ?? "",
|
||||||
|
enabled: editingModel?.enabled ?? true,
|
||||||
contextLimit: editingModel ? String(editingModel.context_limit) : "",
|
contextLimit: editingModel ? String(editingModel.context_limit) : "",
|
||||||
compressionThreshold: editingModel
|
compressionThreshold: editingModel
|
||||||
? String(editingModel.compression_threshold)
|
? String(editingModel.compression_threshold)
|
||||||
|
|||||||
Reference in New Issue
Block a user