diff --git a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.integration.test.tsx
index cf5cad564ea..f6c940a2a71 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.integration.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.integration.test.tsx
@@ -184,6 +184,63 @@ it("persists a List setting typed as comma-separated text as a trimmed string ar
]);
});
+it("persists a String setting typed into its text input", async () => {
+ vi.mocked(getGeneralSettingsCall).mockResolvedValue([
+ {
+ field_name: "maximum_daily_tag_spend_retention_period",
+ field_type: "String",
+ field_value: null,
+ field_description: "retention",
+ stored_in_db: null,
+ },
+ ]);
+ vi.mocked(updateConfigFieldSetting).mockClear();
+ const user = userEvent.setup();
+ renderWithProviders();
+ await user.click(screen.getByRole("tab", { name: "General" }));
+ const input = await screen.findByRole("textbox", { name: "maximum_daily_tag_spend_retention_period" });
+ expect(input).toHaveValue("");
+ fireEvent.change(input, { target: { value: "30d" } });
+ await user.click(
+ within(screen.getByRole("row", { name: /maximum_daily_tag_spend_retention_period/ })).getByRole("button", {
+ name: "Update",
+ }),
+ );
+ expect(vi.mocked(updateConfigFieldSetting).mock.calls).toEqual([
+ ["token", "maximum_daily_tag_spend_retention_period", "30d"],
+ ]);
+ expect(screen.getByText("In DB")).toBeInTheDocument();
+});
+
+it("clears a stored String setting when Update is clicked on an emptied input", async () => {
+ vi.mocked(getGeneralSettingsCall).mockResolvedValue([
+ {
+ field_name: "maximum_daily_tag_spend_retention_period",
+ field_type: "String",
+ field_value: "30d",
+ field_description: "retention",
+ stored_in_db: true,
+ },
+ ]);
+ vi.mocked(updateConfigFieldSetting).mockClear();
+ vi.mocked(deleteConfigFieldSetting).mockClear();
+ const user = userEvent.setup();
+ renderWithProviders();
+ await user.click(screen.getByRole("tab", { name: "General" }));
+ const input = await screen.findByRole("textbox", { name: "maximum_daily_tag_spend_retention_period" });
+ expect(input).toHaveValue("30d");
+ fireEvent.change(input, { target: { value: "" } });
+ await user.click(
+ within(screen.getByRole("row", { name: /maximum_daily_tag_spend_retention_period/ })).getByRole("button", {
+ name: "Update",
+ }),
+ );
+ expect(vi.mocked(deleteConfigFieldSetting).mock.calls).toEqual([
+ ["token", "maximum_daily_tag_spend_retention_period"],
+ ]);
+ expect(updateConfigFieldSetting).not.toHaveBeenCalled();
+});
+
it("clears a stored List setting when Update is clicked on an emptied input", async () => {
vi.mocked(getGeneralSettingsCall).mockResolvedValue([
{
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx
index 3d0a2e438ac..ad006f9dd6d 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/router-settings/_components/general_settings.tsx
@@ -114,6 +114,15 @@ const SettingValueEditor: React.FC<{
/>
);
}
+ if (setting.field_type === "String") {
+ return (
+ onChange(setting.field_name, event.target.value === "" ? null : event.target.value)}
+ />
+ );
+ }
if (setting.field_type === "Select") {
return (