mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
Merge cda0fd6ba5 into b18188dee2
This commit is contained in:
commit
fc3a554e5e
7 changed files with 200 additions and 22 deletions
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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: (
|
||||
<div className="flex w-full items-center justify-between gap-6 py-0.5">
|
||||
<span className="flex flex-col">
|
||||
<span className="font-medium">Moyai</span>{" "}
|
||||
<span className="text-xs leading-snug text-muted-foreground">Cloud Coding Agent</span>
|
||||
</span>
|
||||
{isMoyaiRoute && <Check className="size-4 text-info" />}
|
||||
</div>
|
||||
),
|
||||
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: (
|
||||
<div className="flex w-full items-center justify-between gap-6 py-0.5">
|
||||
<span className="flex flex-col">
|
||||
<span className="font-medium">Moyai</span>{" "}
|
||||
<span className="text-xs leading-snug text-muted-foreground">Cloud Coding Agent</span>
|
||||
</span>
|
||||
{isMoyaiRoute && <Check className="size-4 text-info" />}
|
||||
</div>
|
||||
),
|
||||
onClick: () => window.location.assign(moyaiUrl ?? uiHref(MOYAI)),
|
||||
},
|
||||
...(moyaiEnabled ? [moyaiItem] : []),
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -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(<UISettings />);
|
||||
|
||||
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(<UISettings />);
|
||||
|
||||
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(<UISettings />);
|
||||
|
||||
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(<UISettings />);
|
||||
|
||||
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?.();
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<SettingRow
|
||||
checked={isMoyaiEnabled}
|
||||
disabled={isUpdating || moyaiConfigOwned}
|
||||
onCheckedChange={handleToggleEnableMoyaiUI}
|
||||
ariaLabel={t("adminPanel.uiSettings.settings.enableMoyai.ariaLabel")}
|
||||
label={t("adminPanel.uiSettings.settings.enableMoyai.label")}
|
||||
description={
|
||||
enableMoyaiUIProperty?.description ?? t("adminPanel.uiSettings.settings.enableMoyai.description")
|
||||
}
|
||||
/>
|
||||
{moyaiConfigOwned && (
|
||||
<p className="ml-11 text-sm text-muted-foreground">
|
||||
{t("adminPanel.uiSettings.settings.enableMoyai.managedByConfig")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BackgroundChatSettings />
|
||||
|
||||
|
|
@ -454,17 +496,14 @@ export default function UISettings() {
|
|||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-foreground">Moyai</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{moyaiUrl
|
||||
? t("adminPanel.uiSettings.moyai.connectedTo", { url: moyaiUrl })
|
||||
: t("adminPanel.uiSettings.moyai.notConnected")}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">{moyaiStatus}</p>
|
||||
</div>
|
||||
{moyaiUrl ? (
|
||||
{moyaiUrl && (
|
||||
<Button variant="outline" size="sm" disabled={isUpdating} onClick={handleDisconnectMoyai}>
|
||||
{t("adminPanel.uiSettings.moyai.disconnect")}
|
||||
</Button>
|
||||
) : (
|
||||
)}
|
||||
{!moyaiUrl && isMoyaiEnabled && (
|
||||
<a href={uiHref("moyai")} className="text-sm text-primary underline underline-offset-4">
|
||||
{t("adminPanel.uiSettings.moyai.connectLink")}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue