diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py
index e68008135aa..a7f7ee20a1a 100644
--- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py
+++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py
@@ -375,6 +375,11 @@ class UISettings(LiteLLMBaseModel):
description="If true, shows the Chat page in the UI sidebar, letting users chat with an LLM and connect their own MCP server credentials via OAuth.",
)
+ enable_moyai_ui: bool = Field(
+ default=True,
+ description="If true, shows Moyai in the UI workspace switcher and lets admins connect a Moyai deployment.",
+ )
+
moyai_url: str | None = Field(
default=None,
description="URL of a connected Moyai deployment. When set, the Moyai entry in the UI navigation opens this deployment instead of the Moyai landing page.",
@@ -430,6 +435,7 @@ ALLOWED_UI_SETTINGS_FIELDS: Final = {
"disable_custom_api_keys",
"disable_key_generate_for_org_admin",
"enable_chat_ui",
+ "enable_moyai_ui",
"moyai_url",
TEAM_ADMIN_EDITABLE_TEAM_FIELDS_SETTING,
}
diff --git a/tests/unit/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py b/tests/unit/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py
index 77d0132ec72..da1ef41f722 100644
--- a/tests/unit/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py
+++ b/tests/unit/proxy/ui_crud_endpoints/test_proxy_setting_endpoints.py
@@ -4054,6 +4054,44 @@ class TestEnableChatUISetting:
assert response.json()["values"]["enable_chat_ui"] is False
+class TestEnableMoyaiUISetting:
+ def test_moyai_ui_is_on_when_nothing_is_stored(self, mock_auth, monkeypatch):
+ TestPtuCostAttributionUISetting._mock_prisma(monkeypatch)
+
+ response = client.get("/get/ui_settings")
+
+ assert response.status_code == 200
+ assert response.json()["values"]["enable_moyai_ui"] is True
+
+ def test_admin_can_turn_moyai_ui_off(self, mock_auth, monkeypatch):
+ TestPtuCostAttributionUISetting._mock_prisma(monkeypatch, stored={"enable_moyai_ui": False})
+
+ response = client.get("/get/ui_settings")
+
+ assert response.status_code == 200
+ assert response.json()["values"]["enable_moyai_ui"] is False
+
+ def test_admin_can_patch_moyai_ui_off(self, mock_auth, monkeypatch):
+ from litellm.proxy._types import UserAPIKeyAuth
+ from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
+
+ app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
+ user_id="test-user-123",
+ user_role=LitellmUserRoles.PROXY_ADMIN,
+ )
+ monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", True)
+ mock_prisma = TestPtuCostAttributionUISetting._mock_prisma(monkeypatch)
+
+ try:
+ response = client.patch("/update/ui_settings", json={"enable_moyai_ui": False})
+ finally:
+ app.dependency_overrides.clear()
+
+ assert response.status_code == 200
+ upsert_data = mock_prisma.db.litellm_uisettings.upsert.call_args.kwargs["data"]
+ assert json.loads(upsert_data["create"]["ui_settings"])["enable_moyai_ui"] is False
+
+
class TestApplyUserBudgetToTeamKeysUISetting:
"""``apply_user_budget_to_team_keys`` mirrors general_settings on every GET.
diff --git a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.integration.test.tsx b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.integration.test.tsx
index 64bb8f15585..9d480ababcb 100644
--- a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.integration.test.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.integration.test.tsx
@@ -179,7 +179,9 @@ describe("ViewSwitcher", () => {
it("should switch from Chat to the connected Moyai deployment", async () => {
state.pathname = "/ui/chat";
- const settings = { values: { enable_chat_ui: true, moyai_url: "https://moyai.example.com" } };
+ const settings = {
+ values: { enable_chat_ui: true, enable_moyai_ui: true, moyai_url: "https://moyai.example.com" },
+ };
getUiSettingsMock.mockResolvedValue(settings);
const user = userEvent.setup();
renderSwitcher();
@@ -189,7 +191,20 @@ describe("ViewSwitcher", () => {
expect(assignSpy).toHaveBeenCalledWith("https://moyai.example.com");
});
+ it("should hide Moyai from the switcher when an admin turns it off", async () => {
+ getUiSettingsMock.mockResolvedValue({
+ values: { enable_chat_ui: true, enable_moyai_ui: false, moyai_url: "https://moyai.example.com" },
+ });
+ const user = userEvent.setup();
+ renderSwitcher();
+
+ await user.click(screen.getByRole("button", { name: "Switch workspace, current: AI Gateway" }));
+ await screen.findByRole("menuitem", { name: "Chat Chat UI for using LLMs, MCPs, Agents" });
+ expect(screen.queryByRole("menuitem", { name: /Moyai/ })).not.toBeInTheDocument();
+ });
+
it("should keep the local Moyai route available when Chat is disabled", async () => {
+ getUiSettingsMock.mockResolvedValue({ values: { enable_chat_ui: false, enable_moyai_ui: true } });
const user = userEvent.setup();
renderSwitcher();
diff --git a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx
index aa860226e3b..cba272788fc 100644
--- a/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx
+++ b/ui/litellm-dashboard/src/components/Navbar/ViewSwitcher.tsx
@@ -54,6 +54,7 @@ export default function ViewSwitcher({ className, contentClassName }: ViewSwitch
const pathname = usePathname();
const chatEnabled = Boolean(uiSettings?.values?.enable_chat_ui);
+ const moyaiEnabled = Boolean(uiSettings?.values?.enable_moyai_ui);
const moyaiUrl = (uiSettings?.values?.moyai_url as string | undefined) ?? null;
const normalizedPathname = (pathname ?? "").replace(/\/+$/, "");
@@ -101,6 +102,20 @@ export default function ViewSwitcher({ className, contentClassName }: ViewSwitch
onClick: chatEnabled ? () => window.location.assign(uiHref(CHAT)) : undefined,
};
+ const moyaiItem: ViewSwitcherItem = {
+ key: MOYAI,
+ label: (
+
+
+ Moyai{" "}
+ Cloud Coding Agent
+
+ {isMoyaiRoute && }
+
+ ),
+ onClick: () => window.location.assign(moyaiUrl ?? uiHref(MOYAI)),
+ };
+
const items: ViewSwitcherItem[] = [
...modeEntries.map((e) => ({
key: e.key,
@@ -113,19 +128,7 @@ export default function ViewSwitcher({ className, contentClassName }: ViewSwitch
onClick: () => selectMode(e.key),
})),
chatItem,
- {
- key: MOYAI,
- label: (
-
-
- Moyai{" "}
- Cloud Coding Agent
-
- {isMoyaiRoute && }
-
- ),
- onClick: () => window.location.assign(moyaiUrl ?? uiHref(MOYAI)),
- },
+ ...(moyaiEnabled ? [moyaiItem] : []),
];
return (
diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.test.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.test.tsx
index c2bfd6076d4..4aade313698 100644
--- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.test.tsx
+++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.test.tsx
@@ -173,7 +173,11 @@ describe("UISettings", () => {
buildSettingsResponse({
data: {
...buildSettingsResponse().data,
- values: { ...buildSettingsResponse().data.values, moyai_url: "https://moyai.example.com" },
+ values: {
+ ...buildSettingsResponse().data.values,
+ enable_moyai_ui: true,
+ moyai_url: "https://moyai.example.com",
+ },
},
}),
);
@@ -192,12 +196,78 @@ describe("UISettings", () => {
});
it("shows a link to the Moyai page when not connected", () => {
+ mockUseUISettings.mockReturnValue(
+ buildSettingsResponse({
+ data: {
+ ...buildSettingsResponse().data,
+ values: { ...buildSettingsResponse().data.values, enable_moyai_ui: true },
+ },
+ }),
+ );
render();
expect(screen.getByText("Not connected")).toBeInTheDocument();
expect(screen.getByRole("link", { name: "Connect from the Moyai page" })).toHaveAttribute("href", "/ui/moyai");
});
+ it("shows Moyai as disabled without a connect link when an admin turns it off", () => {
+ mockUseUISettings.mockReturnValue(
+ buildSettingsResponse({
+ data: {
+ ...buildSettingsResponse().data,
+ values: { ...buildSettingsResponse().data.values, enable_moyai_ui: false },
+ },
+ }),
+ );
+ render();
+
+ expect(screen.getByText("Disabled")).toBeInTheDocument();
+ expect(screen.queryByText("Not connected")).not.toBeInTheDocument();
+ expect(screen.queryByRole("link", { name: "Connect from the Moyai page" })).not.toBeInTheDocument();
+ });
+
+ it("locks the Moyai switch when the config file owns enable_moyai_ui", () => {
+ mockUseUISettings.mockReturnValue(
+ buildSettingsResponse({
+ data: {
+ ...buildSettingsResponse().data,
+ values: { ...buildSettingsResponse().data.values, enable_moyai_ui: false },
+ source: { enable_moyai_ui: "config" },
+ },
+ }),
+ );
+ render();
+
+ expect(screen.getByRole("switch", { name: "Enable Moyai" })).toHaveAttribute("aria-disabled", "true");
+ expect(screen.getByText(/Managed by general_settings.enable_moyai_ui in the config file/)).toBeInTheDocument();
+ });
+
+ it("turns Moyai off by patching enable_moyai_ui", () => {
+ const mutateMock = vi.fn((_settings, options) => {
+ options?.onSuccess?.();
+ });
+ mockUseUpdateUISettings.mockReturnValue({ mutate: mutateMock, isPending: false, error: null });
+ mockUseUISettings.mockReturnValue(
+ buildSettingsResponse({
+ data: {
+ ...buildSettingsResponse().data,
+ values: { ...buildSettingsResponse().data.values, enable_moyai_ui: true },
+ },
+ }),
+ );
+ render();
+
+ act(() => {
+ fireEvent.click(screen.getByRole("switch", { name: "Enable Moyai" }));
+ });
+
+ expect(mutateMock).toHaveBeenCalledWith(
+ { enable_moyai_ui: false },
+ expect.objectContaining({ onSuccess: expect.any(Function) }),
+ );
+ expect(toast.success).toHaveBeenCalledWith("UI settings updated successfully");
+ });
+
it("should toggle require auth for public AI Hub setting and call update", () => {
const mutateMock = vi.fn((_settings, options) => {
options?.onSuccess?.();
diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx
index 6fe86c0f352..d95232d013f 100644
--- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx
+++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx
@@ -1,6 +1,7 @@
"use client";
import { useTranslation } from "react-i18next";
+import type { TFunction } from "i18next";
import { useTerminology } from "@/contexts/TerminologyContext";
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import { useUpdateUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUpdateUISettings";
@@ -53,6 +54,12 @@ function SettingRow({
);
}
+function moyaiStatusLabel(t: TFunction, isEnabled: boolean, url: string | null): string {
+ if (!isEnabled) return t("adminPanel.uiSettings.moyai.disabled");
+ if (url) return t("adminPanel.uiSettings.moyai.connectedTo", { url });
+ return t("adminPanel.uiSettings.moyai.notConnected");
+}
+
export default function UISettings() {
const { t } = useTranslation();
const term = useTerminology();
@@ -68,6 +75,7 @@ export default function UISettings() {
const forwardLLMProviderAuthHeadersProperty = schema?.properties?.forward_llm_provider_auth_headers;
const enableProjectsUIProperty = schema?.properties?.enable_projects_ui;
const enableChatUIProperty = schema?.properties?.enable_chat_ui;
+ const enableMoyaiUIProperty = schema?.properties?.enable_moyai_ui;
const enabledPagesProperty = schema?.properties?.enabled_ui_pages_internal_users;
const disableAgentsProperty = schema?.properties?.disable_agents_for_internal_users;
const allowAgentsTeamAdminsProperty = schema?.properties?.allow_agents_for_team_admins;
@@ -81,6 +89,9 @@ export default function UISettings() {
"last_7_days";
const usageRangeConfigOwned = data?.source?.usage_default_time_range === "config";
const moyaiUrl = (values.moyai_url as string | undefined) ?? null;
+ const isMoyaiEnabled = Boolean(values.enable_moyai_ui);
+ const moyaiConfigOwned = data?.source?.enable_moyai_ui === "config";
+ const moyaiStatus = moyaiStatusLabel(t, isMoyaiEnabled, moyaiUrl);
const isDisabledForInternalUsers = Boolean(values.disable_model_add_for_internal_users);
const isDisabledTeamAdminDeleteTeamUser = Boolean(values.disable_team_admin_delete_team_user);
const isAgentsDisabled = Boolean(values.disable_agents_for_internal_users);
@@ -183,6 +194,20 @@ export default function UISettings() {
);
};
+ const handleToggleEnableMoyaiUI = (checked: boolean) => {
+ updateSettings(
+ { enable_moyai_ui: checked },
+ {
+ onSuccess: () => {
+ toast.success(t("adminPanel.uiSettings.toasts.updated"));
+ },
+ onError: (error) => {
+ toast.fromError(error);
+ },
+ },
+ );
+ };
+
const handleToggleRequireAuthForPublicAIHub = (checked: boolean) => {
updateSettings(
{ require_auth_for_public_ai_hub: checked },
@@ -447,6 +472,23 @@ export default function UISettings() {
enableChatUIProperty?.description ?? t("adminPanel.uiSettings.settings.enableChat.description")
}
/>
+
+
+ {moyaiConfigOwned && (
+
+ {t("adminPanel.uiSettings.settings.enableMoyai.managedByConfig")}
+
+ )}
+
@@ -454,17 +496,14 @@ export default function UISettings() {
Moyai
-
- {moyaiUrl
- ? t("adminPanel.uiSettings.moyai.connectedTo", { url: moyaiUrl })
- : t("adminPanel.uiSettings.moyai.notConnected")}
-
+
{moyaiStatus}
- {moyaiUrl ? (
+ {moyaiUrl && (
- ) : (
+ )}
+ {!moyaiUrl && isMoyaiEnabled && (
{t("adminPanel.uiSettings.moyai.connectLink")}
diff --git a/ui/litellm-dashboard/src/i18n/locales/en.json b/ui/litellm-dashboard/src/i18n/locales/en.json
index e98c29e4343..35143ff9d56 100644
--- a/ui/litellm-dashboard/src/i18n/locales/en.json
+++ b/ui/litellm-dashboard/src/i18n/locales/en.json
@@ -2866,6 +2866,12 @@
"label": "Enable Chat page (page will refresh)",
"description": "If enabled, shows the Chat page in the UI sidebar, letting users chat with an LLM and connect their own MCP server credentials via OAuth."
},
+ "enableMoyai": {
+ "ariaLabel": "Enable Moyai",
+ "label": "[BETA] Enable Moyai",
+ "description": "If enabled, shows Moyai in the workspace switcher and lets admins connect a Moyai deployment.",
+ "managedByConfig": "Managed by general_settings.enable_moyai_ui in the config file. Change it there."
+ },
"disableAgents": {
"label": "Disable agents for internal users"
},
@@ -2890,6 +2896,7 @@
"moyai": {
"connectedTo": "Connected to {{url}}",
"notConnected": "Not connected",
+ "disabled": "Disabled",
"disconnect": "Disconnect",
"connectLink": "Connect from the Moyai page"
}