mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-13 08:52:02 +00:00
feat(ui): add deployment-wide Aria visibility setting (#51506)
This commit is contained in:
parent
d6b3c549ee
commit
85afa9a1ca
14 changed files with 191 additions and 34 deletions
|
|
@ -377,6 +377,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.",
|
||||
)
|
||||
|
||||
disable_aria: bool = Field(
|
||||
default=False,
|
||||
description="Hides Aria for everyone on this 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.",
|
||||
|
|
@ -432,6 +437,7 @@ ALLOWED_UI_SETTINGS_FIELDS: Final = {
|
|||
"disable_custom_api_keys",
|
||||
"disable_key_generate_for_org_admin",
|
||||
"enable_chat_ui",
|
||||
"disable_aria",
|
||||
"moyai_url",
|
||||
TEAM_ADMIN_EDITABLE_TEAM_FIELDS_SETTING,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const { getSettings } = vi.hoisted(() => ({
|
|||
getSettings: vi.fn().mockResolvedValue({ values: { enable_projects_ui: true } }),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/networking", () => ({ getUiSettings: getSettings }));
|
||||
vi.mock("@/components/networking", () => ({ getUiSettings: getSettings, getProxyBaseUrl: () => "http://localhost" }));
|
||||
vi.mock("@/components/leftnav", () => ({
|
||||
default: ({ enableProjectsUI }: ComponentProps<typeof Sidebar>) => (
|
||||
<nav>{enableProjectsUI && <a href="/projects">Projects</a>}</nav>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { PTU_FLAG_REFRESH_MS, usePtuCostAttributionEnabled } from "./usePtuCostA
|
|||
import { useUISettings } from "./useUISettings";
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
getProxyBaseUrl: () => "http://localhost",
|
||||
getUiSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { useUISettings } from "./useUISettings";
|
|||
|
||||
// Mock the networking function
|
||||
vi.mock("@/components/networking", () => ({
|
||||
getProxyBaseUrl: () => "http://localhost",
|
||||
getUiSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
|
|
@ -56,7 +57,7 @@ describe("useUISettings", () => {
|
|||
|
||||
expect(result.current.data).toEqual(mockUISettings);
|
||||
expect(result.current.error).toBeNull();
|
||||
expect(getUiSettings).toHaveBeenCalledWith();
|
||||
expect(getUiSettings).toHaveBeenCalledWith("http://localhost");
|
||||
expect(getUiSettings).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
|
@ -80,7 +81,7 @@ describe("useUISettings", () => {
|
|||
|
||||
expect(result.current.error).toEqual(testError);
|
||||
expect(result.current.data).toBeUndefined();
|
||||
expect(getUiSettings).toHaveBeenCalledWith();
|
||||
expect(getUiSettings).toHaveBeenCalledWith("http://localhost");
|
||||
expect(getUiSettings).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
|
@ -97,7 +98,7 @@ describe("useUISettings", () => {
|
|||
});
|
||||
|
||||
expect(result.current.data).toEqual({});
|
||||
expect(getUiSettings).toHaveBeenCalledWith();
|
||||
expect(getUiSettings).toHaveBeenCalledWith("http://localhost");
|
||||
});
|
||||
|
||||
it("should handle network timeout error", async () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { getUiSettings } from "@/components/networking";
|
||||
import { getProxyBaseUrl, getUiSettings } from "@/components/networking";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createQueryKeys } from "../common/queryKeysFactory";
|
||||
|
||||
|
|
@ -13,12 +13,14 @@ const uiSettingsKeys = createQueryKeys("uiSettings");
|
|||
* cached copy stale; a screen that stays mounted and focused never refetches on its own,
|
||||
* so a caller that needs to notice a change also has to poll.
|
||||
*/
|
||||
export const useUISettings = (options?: { staleTime?: number; refetchInterval?: number }) => {
|
||||
export const useUISettings = (options?: { staleTime?: number; refetchInterval?: number; enabled?: boolean }) => {
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
return useQuery<Record<string, any>>({
|
||||
queryKey: uiSettingsKeys.list({}),
|
||||
queryFn: async () => await getUiSettings(),
|
||||
queryKey: [...uiSettingsKeys.list({}), proxyBaseUrl],
|
||||
queryFn: async () => await getUiSettings(proxyBaseUrl),
|
||||
staleTime: options?.staleTime ?? 60 * 60 * 1000, // 1 hour - data rarely changes
|
||||
gcTime: 60 * 60 * 1000, // 1 hour - keep in cache for 1 hour
|
||||
refetchInterval: options?.refetchInterval,
|
||||
enabled: options?.enabled,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -101,6 +101,31 @@ describe("UISettings", () => {
|
|||
expect(screen.getByRole("switch", { name: "Require authentication for public AI Hub" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([false, true])("saves the deployment Aria visibility toggle from %s", (disabled) => {
|
||||
const mutate = vi.fn();
|
||||
mockUseUISettings.mockReturnValue(buildSettingsResponse({ data: { values: { disable_aria: disabled } } }));
|
||||
mockUseUpdateUISettings.mockReturnValue({ mutate, isPending: false, error: null });
|
||||
render(<UISettings />);
|
||||
const toggle = screen.getByRole("switch", { name: "Hide Aria Agent for everyone" });
|
||||
if (disabled) expect(toggle).toBeChecked();
|
||||
else expect(toggle).not.toBeChecked();
|
||||
fireEvent.click(toggle);
|
||||
expect(mutate).toHaveBeenCalledWith({ disable_aria: !disabled }, expect.any(Object));
|
||||
});
|
||||
|
||||
it("keeps configuration-owned Aria visibility read-only", () => {
|
||||
mockUseUISettings.mockReturnValue(
|
||||
buildSettingsResponse({ data: { values: { disable_aria: true }, source: { disable_aria: "config" } } }),
|
||||
);
|
||||
render(<UISettings />);
|
||||
expect(screen.getByRole("switch", { name: "Hide Aria Agent for everyone" })).toBeChecked();
|
||||
expect(screen.getByRole("switch", { name: "Hide Aria Agent for everyone" })).toHaveAttribute(
|
||||
"aria-disabled",
|
||||
"true",
|
||||
);
|
||||
expect(screen.getByText("Managed by the deployment configuration.")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should toggle setting and call update", () => {
|
||||
const mutateMock = vi.fn((_settings, options) => {
|
||||
options?.onSuccess?.();
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ export default function UISettings() {
|
|||
relativeTimeOptions.find((option) => option.settingValue === values.usage_default_time_range)?.settingValue ??
|
||||
"last_7_days";
|
||||
const usageRangeConfigOwned = data?.source?.usage_default_time_range === "config";
|
||||
const ariaConfigOwned = data?.source?.disable_aria === "config";
|
||||
const moyaiUrl = (values.moyai_url as string | undefined) ?? null;
|
||||
const isDisabledForInternalUsers = Boolean(values.disable_model_add_for_internal_users);
|
||||
const isDisabledTeamAdminDeleteTeamUser = Boolean(values.disable_team_admin_delete_team_user);
|
||||
|
|
@ -364,6 +365,29 @@ export default function UISettings() {
|
|||
<p className="text-sm text-muted-foreground">{t("adminPanel.uiSettings.usageRange.managedByConfig")}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<SettingRow
|
||||
checked={values.disable_aria === true}
|
||||
disabled={isUpdating || ariaConfigOwned}
|
||||
onCheckedChange={(checked) =>
|
||||
updateSettings(
|
||||
{ disable_aria: checked },
|
||||
{
|
||||
onSuccess: () => toast.success(t("adminPanel.uiSettings.toasts.updated")),
|
||||
onError: (error) => toast.fromError(error),
|
||||
},
|
||||
)
|
||||
}
|
||||
ariaLabel={t("adminPanel.uiSettings.settings.disableAria.label")}
|
||||
label={t("adminPanel.uiSettings.settings.disableAria.label")}
|
||||
description={t("adminPanel.uiSettings.settings.disableAria.description")}
|
||||
/>
|
||||
{ariaConfigOwned && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("adminPanel.uiSettings.settings.disableAria.managedByConfig")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<SettingRow
|
||||
checked={isDisabledForInternalUsers}
|
||||
disabled={isUpdating}
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ interface GatewayOptions {
|
|||
read?: () => Promise<Response>;
|
||||
usage?: () => Promise<Response>;
|
||||
settings?: { target: string; status: number } | ((request: Request) => Promise<Response>);
|
||||
uiSettings?: () => Promise<Response>;
|
||||
useSiTerminology?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -191,6 +192,7 @@ function gateway(
|
|||
if (typeof settings === "function") return settings(request);
|
||||
return json({ PROXY_BASE_URL: MANAGEMENT, LITELLM_UI_API_DOC_BASE_URL: settings.target }, settings.status);
|
||||
}
|
||||
if (path.endsWith("/get/ui_settings")) return options.uiSettings ? options.uiSettings() : json({ values: {} });
|
||||
if (path.endsWith("/model_group/info"))
|
||||
return json({
|
||||
data: options.models ?? [
|
||||
|
|
@ -254,6 +256,57 @@ afterEach(() => {
|
|||
});
|
||||
|
||||
describe("Aria Agent in the gateway", () => {
|
||||
it.each([
|
||||
["proxy_admin", true],
|
||||
["proxy_admin", false],
|
||||
["proxy_admin_viewer", true],
|
||||
["internal_user", true],
|
||||
["internal_user_viewer", false],
|
||||
["org_admin", true],
|
||||
] as const)("hides every Aria entry point for %s with premium=%s when disabled", async (role, premium) => {
|
||||
session(role, "first-admin", { premium_user: premium });
|
||||
const requests = gateway([], { uiSettings: async () => json({ values: { disable_aria: true } }) });
|
||||
const { client } = renderWidget(undefined, false, true);
|
||||
await screen.findByText("Session ready");
|
||||
await waitFor(() => expect(requests.some((request) => request.url === `${MANAGEMENT}/get/ui_settings`)).toBe(true));
|
||||
await waitFor(() => expect(client.isFetching()).toBe(0));
|
||||
expect(screen.queryByRole("link", { name: "Aria Agent" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("link", { name: "Ask Aria" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByPlaceholderText("Ask Aria…")).not.toBeInTheDocument();
|
||||
fireEvent.keyDown(document, { key: "j", metaKey: true });
|
||||
fireEvent.keyDown(document, { key: "j", ctrlKey: true });
|
||||
expect(push).not.toHaveBeenCalled();
|
||||
expect(requests.some((request) => request.url.endsWith("/models"))).toBe(false);
|
||||
});
|
||||
|
||||
it("waits for settings and does not show Aria when the initial read fails", async () => {
|
||||
const settings = deferred<Response>();
|
||||
gateway([], { uiSettings: () => settings.promise });
|
||||
const { client } = renderWidget(undefined, false, true);
|
||||
await screen.findByText("Session ready");
|
||||
expect(screen.queryByRole("link", { name: "Aria Agent" })).not.toBeInTheDocument();
|
||||
await act(async () => settings.resolve(json({ error: "Unavailable" }, 503)));
|
||||
await waitFor(() => expect(client.isFetching()).toBe(0));
|
||||
expect(screen.queryByRole("link", { name: "Ask Aria" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByPlaceholderText("Ask Aria…")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("waits for the destination gateway's settings instead of reusing an enabled gateway", async () => {
|
||||
const settings = deferred<Response>();
|
||||
gateway([], {
|
||||
uiSettings: async () =>
|
||||
localStorage.getItem("litellm_worker_url") ? settings.promise : json({ values: { disable_aria: false } }),
|
||||
});
|
||||
const view = renderWidget(undefined, false, true, { showPage: false });
|
||||
await screen.findByRole("link", { name: "Ask Aria" });
|
||||
switchToWorkerUrl("https://other-gateway.test");
|
||||
view.refresh();
|
||||
expect(screen.queryByRole("link", { name: "Aria Agent" })).not.toBeInTheDocument();
|
||||
await act(async () => settings.resolve(json({ values: { disable_aria: true } })));
|
||||
await waitFor(() => expect(view.client.isFetching()).toBe(0));
|
||||
expect(screen.queryByRole("link", { name: "Ask Aria" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
"keeps the answer, draft and model when returning to Aria with premium=%s",
|
||||
async (premium) => {
|
||||
|
|
@ -643,7 +696,7 @@ describe("Aria Agent in the gateway", () => {
|
|||
const { client } = renderWidget(undefined, true);
|
||||
await screen.findByText("Session ready");
|
||||
await waitFor(() => expect(client.isFetching()).toBe(0));
|
||||
expect(screen.getByRole("link", { name: "Aria Agent" })).toBeVisible();
|
||||
expect(await screen.findByRole("link", { name: "Aria Agent" })).toBeVisible();
|
||||
expect(screen.queryByRole("region", { name: "Ask Aria Agent", hidden: true })).not.toBeInTheDocument();
|
||||
await waitFor(() => expect(screen.getByPlaceholderText("Ask Aria…")).toBeEnabled());
|
||||
act(() => send("Show my usage"));
|
||||
|
|
@ -1040,19 +1093,20 @@ describe("Aria Agent in the gateway", () => {
|
|||
});
|
||||
|
||||
it.each(
|
||||
["account", "gateway", "destination", "permission"].flatMap((transition) =>
|
||||
["account", "gateway", "destination", "permission", "visibility"].flatMap((transition) =>
|
||||
["pending", "approval", "draft"].map((stage) => [transition, stage] as const),
|
||||
),
|
||||
)("should clear the conversation when %s changes during %s", async (transition, stage) => {
|
||||
const pending = deferred<ModelReply>();
|
||||
const settings = { target: INFERENCE, status: 200 };
|
||||
const visibility = { disable_aria: false };
|
||||
const replies = [
|
||||
answer("Old answer."),
|
||||
...(stage === "pending" ? [pending.promise] : []),
|
||||
...(stage === "approval" ? [toolReply("key_create", keyArguments)] : []),
|
||||
answer("Fresh session."),
|
||||
];
|
||||
const requests = gateway(replies, { settings });
|
||||
const requests = gateway(replies, { settings, uiSettings: async () => json({ values: visibility }) });
|
||||
const view = renderWidget(undefined, true);
|
||||
await openWidget();
|
||||
act(() => send("Old question"));
|
||||
|
|
@ -1079,6 +1133,13 @@ describe("Aria Agent in the gateway", () => {
|
|||
} else if (transition === "destination") {
|
||||
settings.target = INFERENCE + "/updated";
|
||||
await act(async () => view.client.invalidateQueries({ queryKey: ["proxySettings"] }));
|
||||
} else if (transition === "visibility") {
|
||||
visibility.disable_aria = true;
|
||||
await act(async () => view.client.invalidateQueries({ queryKey: ["uiSettings"] }));
|
||||
await waitFor(() => expect(screen.queryByRole("link", { name: "Aria Agent" })).not.toBeInTheDocument());
|
||||
expect(screen.queryByPlaceholderText("Ask Aria…")).not.toBeInTheDocument();
|
||||
visibility.disable_aria = false;
|
||||
await act(async () => view.client.invalidateQueries({ queryKey: ["uiSettings"] }));
|
||||
} else {
|
||||
session("proxy_admin_viewer", "first-admin", { premium_user: true }, "sk-session-first-admin-viewer");
|
||||
view.refresh();
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { RotateCcw } from "lucide-react";
|
|||
import { z } from "zod";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { useDisableLiteAdmin } from "@/app/(dashboard)/hooks/useDisableLiteAdmin";
|
||||
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
|
||||
import { useProxySettingsQuery } from "@/app/(dashboard)/hooks/proxySettings/useProxySettings";
|
||||
import { ChatComposer } from "@/app/(dashboard)/playground/components/chat_ui/ChatComposer";
|
||||
import { EndpointType, isModeCompatibleWithEndpoint } from "@/components/chat_ui/mode_endpoint_mapping";
|
||||
|
|
@ -56,7 +57,9 @@ function useLiteAdminSession() {
|
|||
const auth = useAuthorized();
|
||||
const [disabled] = useDisableLiteAdmin(auth.userId);
|
||||
const sessionReady = !auth.isLoading && auth.isAuthorized;
|
||||
const { data: uiSettings } = useUISettings({ staleTime: 0, refetchInterval: 30_000, enabled: sessionReady });
|
||||
const admin = auth.premiumUser === true && !auth.isViewOnly && isProxyAdminRole(auth.userRole);
|
||||
if (!uiSettings || uiSettings.values?.disable_aria === true) return null;
|
||||
if (!sessionReady || (admin && disabled)) return null;
|
||||
if (!auth.token || !auth.accessToken || !auth.userId) return null;
|
||||
const access: LiteAdminSession["access"] = admin
|
||||
|
|
|
|||
|
|
@ -6802,8 +6802,7 @@ export const exchangeLoginCode = async (code: string, workerBaseUrl?: string | n
|
|||
return data.token;
|
||||
};
|
||||
|
||||
export const getUiSettings = async () => {
|
||||
const proxyBaseUrl = getProxyBaseUrl();
|
||||
export const getUiSettings = async (proxyBaseUrl = getProxyBaseUrl()) => {
|
||||
const url = proxyBaseUrl ? `${proxyBaseUrl}/get/ui_settings` : `/get/ui_settings`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ vi.mock("@/app/(dashboard)/hooks/keys/useResetKeySpend", () => ({
|
|||
}));
|
||||
vi.mock("../networking", () => ({
|
||||
serverRootPath: "",
|
||||
getProxyBaseUrl: vi.fn(() => ""),
|
||||
getUiSettings: vi.fn().mockResolvedValue({ values: {} }),
|
||||
keyDeleteCall: vi.fn().mockResolvedValue({}),
|
||||
keyUpdateCall: vi.fn().mockResolvedValue({}),
|
||||
getPolicyInfoWithGuardrails: vi.fn().mockResolvedValue({ resolved_guardrails: [] }),
|
||||
|
|
|
|||
|
|
@ -98,6 +98,8 @@ import { useMCPToolsets } from "@/app/(dashboard)/hooks/mcpServers/useMCPToolset
|
|||
|
||||
vi.mock("../networking", () => ({
|
||||
serverRootPath: "",
|
||||
getProxyBaseUrl: vi.fn(() => ""),
|
||||
getUiSettings: vi.fn().mockResolvedValue({ values: {} }),
|
||||
keyDeleteCall: vi.fn().mockResolvedValue({}),
|
||||
keyUpdateCall: vi.fn().mockResolvedValue({}),
|
||||
regenerateKeyCall: vi.fn().mockResolvedValue({ key: "sk-regenerated", token: "regenerated-token" }),
|
||||
|
|
|
|||
|
|
@ -5212,6 +5212,11 @@
|
|||
"managedByConfig": "Managed by the deployment configuration."
|
||||
},
|
||||
"settings": {
|
||||
"disableAria": {
|
||||
"label": "Hide Aria Agent for everyone",
|
||||
"description": "Hides Aria from navigation, Usage, and its workspace for all users, including admins. Active dashboards check for changes every 30 seconds.",
|
||||
"managedByConfig": "Managed by the deployment configuration."
|
||||
},
|
||||
"disableModelAdd": {
|
||||
"label": "Disable model add for internal users"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4663,9 +4663,20 @@ class TestMoyaiUrlSetting:
|
|||
assert persisted["moyai_url"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("preset", ["today", "last_7_days", "last_30_days", "month_to_date", "year_to_date"])
|
||||
def test_usage_default_range_round_trips_without_overwriting_other_settings(
|
||||
mock_auth, monkeypatch: pytest.MonkeyPatch, preset: str,
|
||||
@pytest.mark.parametrize("setting,initial_value,submitted,default", (
|
||||
("usage_default_time_range", None, "today", "last_7_days"),
|
||||
("usage_default_time_range", None, "last_7_days", "last_7_days"),
|
||||
("usage_default_time_range", None, "last_30_days", "last_7_days"),
|
||||
("usage_default_time_range", None, "month_to_date", "last_7_days"),
|
||||
("usage_default_time_range", None, "year_to_date", "last_7_days"),
|
||||
("disable_aria", None, False, False),
|
||||
("disable_aria", None, True, False),
|
||||
("disable_aria", False, True, False),
|
||||
("disable_aria", True, False, False),
|
||||
))
|
||||
def test_ui_setting_round_trips_without_overwriting_other_settings(
|
||||
mock_auth: None, monkeypatch: pytest.MonkeyPatch, setting: str,
|
||||
initial_value: str | bool | None, submitted: str | bool, default: str | bool,
|
||||
) -> None:
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
|
@ -4676,7 +4687,7 @@ def test_usage_default_range_round_trips_without_overwriting_other_settings(
|
|||
|
||||
prisma: Final = MagicMock()
|
||||
prisma.db.litellm_uisettings.find_unique = AsyncMock(return_value=SimpleNamespace(
|
||||
ui_settings={"disable_custom_api_keys": True},
|
||||
ui_settings={"disable_custom_api_keys": True, **({} if initial_value is None else {setting: initial_value})},
|
||||
))
|
||||
prisma.db.litellm_uisettings.upsert = AsyncMock()
|
||||
monkeypatch.setattr(proxy_server, "prisma_client", prisma)
|
||||
|
|
@ -4686,22 +4697,28 @@ def test_usage_default_range_round_trips_without_overwriting_other_settings(
|
|||
app.dependency_overrides[user_api_key_auth] = lambda: actor
|
||||
try:
|
||||
initial: Final = client.get("/get/ui_settings")
|
||||
assert initial.json()["values"]["usage_default_time_range"] == "last_7_days"
|
||||
response: Final = client.patch("/update/ui_settings", json={"usage_default_time_range": preset})
|
||||
assert initial.status_code == 200, initial.text
|
||||
assert initial.json()["values"][setting] == (default if initial_value is None else initial_value)
|
||||
assert initial.json()["source"][setting] == ("default" if initial_value is None else "db")
|
||||
response: Final = client.patch("/update/ui_settings", json={setting: submitted})
|
||||
assert response.status_code == 200, response.text
|
||||
stored: Final = json.loads(prisma.db.litellm_uisettings.upsert.call_args.kwargs["data"]["update"]["ui_settings"])
|
||||
assert stored == {"disable_custom_api_keys": True, "usage_default_time_range": preset}
|
||||
prisma.db.litellm_uisettings.find_unique.return_value = SimpleNamespace(ui_settings=stored)
|
||||
assert stored == {"disable_custom_api_keys": True, setting: submitted}
|
||||
prisma.db.litellm_uisettings.find_unique.return_value = SimpleNamespace(ui_settings=json.dumps(stored))
|
||||
fetched: Final = client.get("/get/ui_settings")
|
||||
assert fetched.json()["values"]["usage_default_time_range"] == preset
|
||||
assert fetched.json()["source"]["usage_default_time_range"] == "db"
|
||||
assert fetched.status_code == 200, fetched.text
|
||||
assert fetched.json()["values"][setting] == submitted
|
||||
assert fetched.json()["values"]["disable_custom_api_keys"] is True
|
||||
assert fetched.json()["source"][setting] == "db"
|
||||
assert "disable_aria" not in proxy_server.general_settings
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("submitted", ["invalid", None, 7])
|
||||
def test_usage_default_range_rejects_invalid_values_without_writing(
|
||||
mock_auth, monkeypatch: pytest.MonkeyPatch, submitted: object,
|
||||
@pytest.mark.parametrize("setting", ("usage_default_time_range", "disable_aria"))
|
||||
def test_ui_setting_rejects_invalid_values_without_writing(
|
||||
mock_auth: None, monkeypatch: pytest.MonkeyPatch, submitted: object, setting: str,
|
||||
) -> None:
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from litellm.proxy import proxy_server
|
||||
|
|
@ -4709,13 +4726,14 @@ def test_usage_default_range_rejects_invalid_values_without_writing(
|
|||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
|
||||
prisma: Final = MagicMock()
|
||||
prisma.db.litellm_uisettings.find_unique = AsyncMock(return_value=None)
|
||||
prisma.db.litellm_uisettings.upsert = AsyncMock()
|
||||
monkeypatch.setattr(proxy_server, "prisma_client", prisma)
|
||||
monkeypatch.setattr(proxy_server, "store_model_in_db", True)
|
||||
actor: Final = UserAPIKeyAuth(user_id="test-admin", user_role=LitellmUserRoles.PROXY_ADMIN)
|
||||
app.dependency_overrides[user_api_key_auth] = lambda: actor
|
||||
try:
|
||||
response: Final = client.patch("/update/ui_settings", json={"usage_default_time_range": submitted})
|
||||
response: Final = client.patch("/update/ui_settings", json={setting: submitted})
|
||||
assert response.status_code == 422, response.text
|
||||
prisma.db.litellm_uisettings.upsert.assert_not_called()
|
||||
finally:
|
||||
|
|
@ -4723,8 +4741,9 @@ def test_usage_default_range_rejects_invalid_values_without_writing(
|
|||
|
||||
|
||||
@pytest.mark.parametrize("role", [LitellmUserRoles.INTERNAL_USER, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY])
|
||||
def test_usage_default_range_cannot_be_changed_by_non_admins(
|
||||
mock_auth, monkeypatch: pytest.MonkeyPatch, role: LitellmUserRoles,
|
||||
@pytest.mark.parametrize("setting,value", (("usage_default_time_range", "month_to_date"), ("disable_aria", True)))
|
||||
def test_ui_setting_cannot_be_changed_by_non_admins(
|
||||
mock_auth: None, monkeypatch: pytest.MonkeyPatch, role: LitellmUserRoles, setting: str, value: str | bool,
|
||||
) -> None:
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from litellm.proxy import proxy_server
|
||||
|
|
@ -4738,14 +4757,21 @@ def test_usage_default_range_cannot_be_changed_by_non_admins(
|
|||
actor: Final = UserAPIKeyAuth(user_id="test-user", user_role=role)
|
||||
app.dependency_overrides[user_api_key_auth] = lambda: actor
|
||||
try:
|
||||
response: Final = client.patch("/update/ui_settings", json={"usage_default_time_range": "month_to_date"})
|
||||
response: Final = client.patch("/update/ui_settings", json={setting: value})
|
||||
assert response.status_code == 403, response.text
|
||||
prisma.db.litellm_uisettings.upsert.assert_not_called()
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_usage_default_range_reports_config_precedence(mock_auth, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
@pytest.mark.parametrize("setting,stored,configured", (
|
||||
("usage_default_time_range", "today", "month_to_date"),
|
||||
("disable_aria", False, True),
|
||||
("disable_aria", True, False),
|
||||
))
|
||||
def test_ui_setting_reports_config_precedence(
|
||||
mock_auth: None, monkeypatch: pytest.MonkeyPatch, setting: str, stored: str | bool, configured: str | bool,
|
||||
) -> None:
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from litellm.proxy import proxy_server
|
||||
|
|
@ -4753,16 +4779,16 @@ def test_usage_default_range_reports_config_precedence(mock_auth, monkeypatch: p
|
|||
|
||||
prisma: Final = MagicMock()
|
||||
prisma.db.litellm_uisettings.find_unique = AsyncMock(return_value=SimpleNamespace(
|
||||
ui_settings={"usage_default_time_range": "today"},
|
||||
ui_settings={setting: stored},
|
||||
))
|
||||
monkeypatch.setattr(proxy_server, "prisma_client", prisma)
|
||||
settings: Final = SettingsStore("general_settings")
|
||||
settings.load_yaml({"usage_default_time_range": "month_to_date"})
|
||||
settings.load_yaml({setting: configured})
|
||||
monkeypatch.setattr(proxy_server.proxy_config, "settings", settings)
|
||||
response: Final = client.get("/get/ui_settings")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["values"]["usage_default_time_range"] == "month_to_date"
|
||||
assert response.json()["source"]["usage_default_time_range"] == "config"
|
||||
assert response.json()["values"][setting] == configured
|
||||
assert response.json()["source"][setting] == "config"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue