Skip to content
2 changes: 2 additions & 0 deletions litellm/proxy/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,8 @@ class LiteLLMRoutes(enum.Enum):
"/global/activity/model",
"/v1/models/{model_id}",
"/models/{model_id}",
"/guardrails/list",
"/v2/guardrails/list",
]
+ spend_tracking_routes
+ key_management_routes
Expand Down
34 changes: 26 additions & 8 deletions ui/litellm-dashboard/src/components/networking.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5460,8 +5460,8 @@ export const testMCPSemanticFilter = async (accessToken: string, model: string,

export const getGuardrailsList = async (accessToken: string) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/v2/guardrails/list` : `/v2/guardrails/list`;
const response = await fetch(url, {
const v2Url = proxyBaseUrl ? `${proxyBaseUrl}/v2/guardrails/list` : `/v2/guardrails/list`;
const response = await fetch(v2Url, {
method: "GET",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
Expand All @@ -5470,17 +5470,35 @@ export const getGuardrailsList = async (accessToken: string) => {
});

if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
throw new Error(`v2 guardrails/list returned ${response.status}`);
}

const data = await response.json();
return data;
} catch (error) {
console.error("Failed to get guardrails list:", error);
throw error;
console.log("v2/guardrails/list failed, falling back to v1:", error);
try {
const v1Url = proxyBaseUrl ? `${proxyBaseUrl}/guardrails/list` : `/guardrails/list`;
const fallbackResponse = await fetch(v1Url, {
method: "GET",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});

if (!fallbackResponse.ok) {
const errorData = await fallbackResponse.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}

return await fallbackResponse.json();
} catch (fallbackError) {
console.error("Failed to get guardrails list:", fallbackError);
throw fallbackError;
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export const fetchUserModels = async (
*/
const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
const { accessToken, userId: userID, userRole, premiumUser } = useAuthorized();
const canEditGuardrails = premiumUser || (userRole != null && rolesWithWriteAccess.includes(userRole));
const { data: projects, isLoading: isProjectsLoading } = useProjects();
const { data: uiSettingsData } = useUISettings();
const enableProjectsUI = Boolean(uiSettingsData?.values?.enable_projects_ui);
Expand Down Expand Up @@ -1000,17 +1001,17 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
name="guardrails"
className="mt-4"
help={
premiumUser
canEditGuardrails
? "Select existing guardrails or enter new ones"
: "Premium feature - Upgrade to set guardrails by key"
}
>
<Select
mode="tags"
style={{ width: "100%" }}
disabled={!premiumUser}
disabled={!canEditGuardrails}
placeholder={
!premiumUser
!canEditGuardrails
? "Premium feature - Upgrade to set guardrails by key"
: "Select or enter guardrails"
}
Expand All @@ -1037,12 +1038,12 @@ const CreateKey: React.FC<CreateKeyProps> = ({ team, teams, data, addKey }) => {
className="mt-4"
valuePropName="checked"
help={
premiumUser
canEditGuardrails
? "Bypass global guardrails for this key"
: "Premium feature - Upgrade to disable global guardrails by key"
}
>
<Switch disabled={!premiumUser} checkedChildren="Yes" unCheckedChildren="No" />
<Switch disabled={!canEditGuardrails} checkedChildren="Yes" unCheckedChildren="No" />
</Form.Item>
<Form.Item
label={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,14 @@ vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({
useUISettings: vi.fn().mockReturnValue({ data: { values: {} }, isLoading: false }),
}));

// Mock useResetKeySpend hook (requires QueryClientProvider which is not available in this test)
vi.mock("@/app/(dashboard)/hooks/keys/useResetKeySpend", () => ({
useResetKeySpend: vi.fn().mockReturnValue({
mutate: vi.fn(),
isPending: false,
}),
}));

// KeyEditView mock: triggers onSubmit with our injected form values
vi.mock("./key_edit_view", async () => {
const React = await import("react");
Expand Down Expand Up @@ -334,17 +342,37 @@ beforeEach(() => {
});

// ---- Tests ----
describe("KeyInfoView handleKeyUpdate premium guard", () => {
it("removes guardrails & prompts for non-premium users and prevents metadata.guardrails", async () => {
renderView(false); // premiumUser = false
describe("KeyInfoView handleKeyUpdate guardrails guard", () => {
it("should remove guardrails & prompts for non-premium key owner without write access role", async () => {
const keyDataWithOwner = { ...baseKeyData, user_id: "user_1" };
mockUseAuthorized.mockReturnValue({
accessToken: "access_abc",
userId: "user_1",
userRole: "viewer",
premiumUser: false,
token: "token_123",
userEmail: "test@example.com",
disabledPersonalKeyCreation: false,
showSSOBanner: false,
});

render(
<KeyInfoView
keyId="tok_123"
onClose={() => {}}
keyData={keyDataWithOwner as any}
onKeyDataUpdate={() => {}}
teams={[]}
/>,
);

fireEvent.click(screen.getByText("Settings"));
fireEvent.click(screen.getByText("Edit Settings"));
(globalThis as any).__TEST_FORM_VALUES = {
token: "tok_123",
guardrails: ["gr-1", "gr-2"],
prompts: ["fast", "safe"],
metadata: {}, // object form (not JSON string)
metadata: {},
};

fireEvent.click(screen.getByText("Mock Submit"));
Expand All @@ -360,7 +388,31 @@ describe("KeyInfoView handleKeyUpdate premium guard", () => {
expect(sentPayload.key).toBe("tok_123");
});

it("preserves guardrails & prompts for premium users and includes metadata.guardrails", async () => {
it("should preserve guardrails & prompts for non-premium users with write access role (e.g. Admin)", async () => {
renderView(false); // premiumUser = false, userRole = "Admin"

fireEvent.click(screen.getByText("Settings"));
fireEvent.click(screen.getByText("Edit Settings"));
(globalThis as any).__TEST_FORM_VALUES = {
token: "tok_123",
guardrails: ["gr-1"],
prompts: ["fast"],
metadata: {},
};

fireEvent.click(screen.getByText("Mock Submit"));

await waitFor(() => expect(keyUpdateCallMock).toHaveBeenCalled());

const [, sentPayload] = keyUpdateCallMock.mock.calls[0];

expect(sentPayload.guardrails).toEqual(["gr-1"]);
expect(sentPayload.prompts).toEqual(["fast"]);
expect(sentPayload.metadata?.guardrails).toEqual(["gr-1"]);
expect(sentPayload.key).toBe("tok_123");
});

it("should preserve guardrails & prompts for premium users and includes metadata.guardrails", async () => {
renderView(true); // premiumUser = true

fireEvent.click(screen.getByText("Settings"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ describe("KeyEditView", () => {
});
});

it("should disable guardrails selector when user is not premium", async () => {
it("should disable guardrails selector when user is not premium and has no write access role", async () => {
renderWithProviders(
<KeyEditView
keyData={MOCK_KEY_DATA}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { InfoCircleOutlined } from "@ant-design/icons";
import { TextInput, Button as TremorButton } from "@tremor/react";
import { Form, Input, Select, Switch, Tooltip } from "antd";
import { useEffect, useState } from "react";
import { rolesWithWriteAccess } from "../../utils/roles";
import AgentSelector from "../agent_management/AgentSelector";
import AccessGroupSelector from "../common_components/AccessGroupSelector";
import { mapInternalToDisplayNames } from "../callback_info_helpers";
Expand Down Expand Up @@ -84,6 +85,7 @@ export function KeyEditView({
userRole,
premiumUser = false,
}: KeyEditViewProps) {
const canEditGuardrails = premiumUser || (userRole != null && rolesWithWriteAccess.includes(userRole));
const [form] = Form.useForm();
const [promptsList, setPromptsList] = useState<string[]>([]);
const [tagsList, setTagsList] = useState<Record<string, Tag>>({});
Expand Down Expand Up @@ -443,7 +445,7 @@ export function KeyEditView({
form.setFieldValue("guardrails", v);
}}
accessToken={accessToken}
disabled={!premiumUser}
disabled={!canEditGuardrails}
/>
)}
</Form.Item>
Expand All @@ -460,7 +462,7 @@ export function KeyEditView({
name="disable_global_guardrails"
valuePropName="checked"
>
<Switch disabled={!premiumUser} checkedChildren="Yes" unCheckedChildren="No" />
<Switch disabled={!canEditGuardrails} checkedChildren="Yes" unCheckedChildren="No" />
</Form.Item>

<Form.Item
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Badge, Button, Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels,
import { Form, Modal, Tag } from "antd";
import { KeyInfoHeader } from "./KeyInfoHeader";
import { useEffect, useState } from "react";
import { isProxyAdminRole, isUserTeamAdminForSingleTeam } from "../../utils/roles";
import { isProxyAdminRole, isUserTeamAdminForSingleTeam, rolesWithWriteAccess } from "../../utils/roles";
import { mapDisplayToInternalNames, mapInternalToDisplayNames } from "../callback_info_helpers";
import AutoRotationView from "../common_components/AutoRotationView";
import DeleteResourceModal from "../common_components/DeleteResourceModal";
Expand Down Expand Up @@ -50,6 +50,7 @@ export default function KeyInfoView({
backButtonText = "Back to Keys",
}: KeyInfoViewProps) {
const { accessToken, userId: userID, userRole, premiumUser } = useAuthorized();
const canEditGuardrails = premiumUser || (userRole != null && rolesWithWriteAccess.includes(userRole));
const { teams: teamsData } = useTeams();
const { data: projects } = useProjects();
const { data: uiSettingsData } = useUISettings();
Expand Down Expand Up @@ -140,7 +141,7 @@ export default function KeyInfoView({
formValues.key = currentKey;

// Guard premium features
if (!premiumUser) {
if (!canEditGuardrails) {
delete formValues.guardrails;
delete formValues.prompts;
}
Expand Down
Loading