mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-27 11:43:54 +00:00
(UI) - Set/edit guardrails on a virtual key (#7954)
* Revert "JWT Auth - `enforce_rbac` support + UI team view, spend calc fix (#7863)" This reverts commitdca6904937
. * Revert "Litellm dev 01 10 2025 p2 (#7679)" This reverts commitc4780479a9
. * ui - allow setting guardrails on a key * working edit guardrails * fix edit guardrails on a key * Revert "Revert "JWT Auth - `enforce_rbac` support + UI team view, spend calc fix (#7863)"" This reverts commit8f7b9ae1af
. * Revert "Revert "Litellm dev 01 10 2025 p2 (#7679)"" This reverts commita609139dde
. * fix edit guardrail on ui * fix list_guardrails
This commit is contained in:
parent
e6ec4f21e5
commit
84fb5aead8
7 changed files with 185 additions and 35 deletions
|
@ -2288,7 +2288,6 @@ class ProxyStateVariables(TypedDict):
|
||||||
UI_TEAM_ID = "litellm-dashboard"
|
UI_TEAM_ID = "litellm-dashboard"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class JWTAuthBuilderResult(TypedDict):
|
class JWTAuthBuilderResult(TypedDict):
|
||||||
is_proxy_admin: bool
|
is_proxy_admin: bool
|
||||||
team_object: Optional[LiteLLM_TeamTable]
|
team_object: Optional[LiteLLM_TeamTable]
|
||||||
|
@ -2301,6 +2300,7 @@ class JWTAuthBuilderResult(TypedDict):
|
||||||
end_user_id: Optional[str]
|
end_user_id: Optional[str]
|
||||||
org_id: Optional[str]
|
org_id: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class ClientSideFallbackModel(TypedDict, total=False):
|
class ClientSideFallbackModel(TypedDict, total=False):
|
||||||
"""
|
"""
|
||||||
Dictionary passed when client configuring input
|
Dictionary passed when client configuring input
|
||||||
|
|
|
@ -6,7 +6,6 @@ from typing import Dict, List, Optional, cast
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
|
||||||
from litellm.proxy._types import CommonProxyErrors
|
|
||||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||||
from litellm.types.guardrails import GuardrailInfoResponse, ListGuardrailsResponse
|
from litellm.types.guardrails import GuardrailInfoResponse, ListGuardrailsResponse
|
||||||
|
|
||||||
|
@ -40,7 +39,6 @@ def _get_guardrails_list_response(
|
||||||
)
|
)
|
||||||
async def list_guardrails():
|
async def list_guardrails():
|
||||||
"""
|
"""
|
||||||
✨ Enterprise Feature
|
|
||||||
List the guardrails that are available on the proxy server
|
List the guardrails that are available on the proxy server
|
||||||
|
|
||||||
👉 [Guardrail docs](https://docs.litellm.ai/docs/proxy/guardrails/quick_start)
|
👉 [Guardrail docs](https://docs.litellm.ai/docs/proxy/guardrails/quick_start)
|
||||||
|
@ -74,15 +72,7 @@ async def list_guardrails():
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
"""
|
"""
|
||||||
from litellm.proxy.proxy_server import premium_user, proxy_config
|
from litellm.proxy.proxy_server import proxy_config
|
||||||
|
|
||||||
if not premium_user:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
|
||||||
detail={
|
|
||||||
"error": CommonProxyErrors.not_premium_user.value,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
config = proxy_config.config
|
config = proxy_config.config
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,20 @@ model_list:
|
||||||
api_base: https://exampleopenaiendpoint-production.up.railway.app/
|
api_base: https://exampleopenaiendpoint-production.up.railway.app/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
litellm_settings:
|
litellm_settings:
|
||||||
callbacks: ["prometheus"]
|
callbacks: ["prometheus"]
|
||||||
|
|
||||||
|
|
||||||
|
guardrails:
|
||||||
|
- guardrail_name: "bedrock-pre-guard"
|
||||||
|
litellm_params:
|
||||||
|
guardrail: bedrock # supported values: "aporia", "bedrock", "lakera"
|
||||||
|
mode: "during_call"
|
||||||
|
guardrailIdentifier: ff6ujrregl1q
|
||||||
|
guardrailVersion: "DRAFT"
|
||||||
|
- guardrail_name: "bedrock-post-guard"
|
||||||
|
litellm_params:
|
||||||
|
guardrail: bedrock # supported values: "aporia", "bedrock", "lakera"
|
||||||
|
mode: "post_call"
|
||||||
|
guardrailIdentifier: ff6ujrregl1q
|
||||||
|
guardrailVersion: "DRAFT"
|
|
@ -27,7 +27,10 @@ import {
|
||||||
keyCreateCall,
|
keyCreateCall,
|
||||||
slackBudgetAlertsHealthCheck,
|
slackBudgetAlertsHealthCheck,
|
||||||
modelAvailableCall,
|
modelAvailableCall,
|
||||||
|
getGuardrailsList,
|
||||||
} from "./networking";
|
} from "./networking";
|
||||||
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { Tooltip } from 'antd';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
|
@ -81,7 +84,7 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
||||||
const [modelsToPick, setModelsToPick] = useState([]);
|
const [modelsToPick, setModelsToPick] = useState([]);
|
||||||
const [keyOwner, setKeyOwner] = useState("you");
|
const [keyOwner, setKeyOwner] = useState("you");
|
||||||
const [predefinedTags, setPredefinedTags] = useState(getPredefinedTags(data));
|
const [predefinedTags, setPredefinedTags] = useState(getPredefinedTags(data));
|
||||||
|
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
||||||
|
|
||||||
const handleOk = () => {
|
const handleOk = () => {
|
||||||
setIsModalVisible(false);
|
setIsModalVisible(false);
|
||||||
|
@ -121,6 +124,22 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
||||||
fetchUserModels();
|
fetchUserModels();
|
||||||
}, [accessToken, userID, userRole]);
|
}, [accessToken, userID, userRole]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchGuardrails = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getGuardrailsList(accessToken);
|
||||||
|
const guardrailNames = response.guardrails.map(
|
||||||
|
(g: { guardrail_name: string }) => g.guardrail_name
|
||||||
|
);
|
||||||
|
setGuardrailsList(guardrailNames);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch guardrails:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchGuardrails();
|
||||||
|
}, [accessToken]);
|
||||||
|
|
||||||
const handleCreate = async (formValues: Record<string, any>) => {
|
const handleCreate = async (formValues: Record<string, any>) => {
|
||||||
try {
|
try {
|
||||||
const newKeyAlias = formValues?.key_alias ?? "";
|
const newKeyAlias = formValues?.key_alias ?? "";
|
||||||
|
@ -392,6 +411,33 @@ const CreateKey: React.FC<CreateKeyProps> = ({
|
||||||
>
|
>
|
||||||
<TextInput placeholder="" />
|
<TextInput placeholder="" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
Guardrails{' '}
|
||||||
|
<Tooltip title="Setup your first guardrail">
|
||||||
|
<a
|
||||||
|
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
onClick={(e) => e.stopPropagation()} // Prevent accordion from collapsing when clicking link
|
||||||
|
>
|
||||||
|
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
name="guardrails"
|
||||||
|
className="mt-8"
|
||||||
|
help="Select existing guardrails or enter new ones"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode="tags"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="Select or enter guardrails"
|
||||||
|
options={guardrailsList.map(name => ({ value: name, label: name }))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="Metadata" name="metadata" className="mt-8">
|
<Form.Item label="Metadata" name="metadata" className="mt-8">
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
|
|
|
@ -3050,3 +3050,31 @@ export const getProxyUISettings = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const getGuardrailsList = async (accessToken: String) => {
|
||||||
|
try {
|
||||||
|
let url = proxyBaseUrl ? `${proxyBaseUrl}/guardrails/list` : `/guardrails/list`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.text();
|
||||||
|
handleError(errorData);
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("Guardrails list response:", data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch guardrails list:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -206,7 +206,6 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||||
|
|
||||||
setUserSpendData(response["user_info"]);
|
setUserSpendData(response["user_info"]);
|
||||||
console.log(`userSpendData: ${JSON.stringify(userSpendData)}`)
|
console.log(`userSpendData: ${JSON.stringify(userSpendData)}`)
|
||||||
|
|
||||||
const teamsArray = [...response["teams"]];
|
const teamsArray = [...response["teams"]];
|
||||||
if (teamsArray.length > 0) {
|
if (teamsArray.length > 0) {
|
||||||
console.log(`response['teams']: ${JSON.stringify(teamsArray)}`);
|
console.log(`response['teams']: ${JSON.stringify(teamsArray)}`);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { keyDeleteCall, modelAvailableCall } from "./networking";
|
import { keyDeleteCall, modelAvailableCall, getGuardrailsList } from "./networking";
|
||||||
import { add } from 'date-fns';
|
import { add } from 'date-fns';
|
||||||
import { InformationCircleIcon, StatusOnlineIcon, TrashIcon, PencilAltIcon, RefreshIcon } from "@heroicons/react/outline";
|
import { InformationCircleIcon, StatusOnlineIcon, TrashIcon, PencilAltIcon, RefreshIcon } from "@heroicons/react/outline";
|
||||||
import { keySpendLogsCall, PredictedSpendLogsCall, keyUpdateCall, modelInfoCall, regenerateKeyCall } from "./networking";
|
import { keySpendLogsCall, PredictedSpendLogsCall, keyUpdateCall, modelInfoCall, regenerateKeyCall } from "./networking";
|
||||||
|
@ -26,6 +26,7 @@ import {
|
||||||
TextInput,
|
TextInput,
|
||||||
Textarea,
|
Textarea,
|
||||||
} from "@tremor/react";
|
} from "@tremor/react";
|
||||||
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import { Select as Select3, SelectItem, MultiSelect, MultiSelectItem } from "@tremor/react";
|
import { Select as Select3, SelectItem, MultiSelect, MultiSelectItem } from "@tremor/react";
|
||||||
import {
|
import {
|
||||||
Button as Button2,
|
Button as Button2,
|
||||||
|
@ -130,6 +131,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
const [newExpiryTime, setNewExpiryTime] = useState<string | null>(null);
|
const [newExpiryTime, setNewExpiryTime] = useState<string | null>(null);
|
||||||
|
|
||||||
const [knownTeamIDs, setKnownTeamIDs] = useState(initialKnownTeamIDs);
|
const [knownTeamIDs, setKnownTeamIDs] = useState(initialKnownTeamIDs);
|
||||||
|
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
||||||
|
|
||||||
// Function to check if user is admin of a team
|
// Function to check if user is admin of a team
|
||||||
const isUserTeamAdmin = (team: any) => {
|
const isUserTeamAdmin = (team: any) => {
|
||||||
|
@ -311,22 +313,49 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
const [keyTeam, setKeyTeam] = useState(selectedTeam);
|
const [keyTeam, setKeyTeam] = useState(selectedTeam);
|
||||||
const [errorModels, setErrorModels] = useState<string[]>([]);
|
const [errorModels, setErrorModels] = useState<string[]>([]);
|
||||||
const [errorBudget, setErrorBudget] = useState<boolean>(false);
|
const [errorBudget, setErrorBudget] = useState<boolean>(false);
|
||||||
|
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchGuardrails = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getGuardrailsList(accessToken);
|
||||||
|
const guardrailNames = response.guardrails.map(
|
||||||
|
(g: { guardrail_name: string }) => g.guardrail_name
|
||||||
|
);
|
||||||
|
setGuardrailsList(guardrailNames);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch guardrails:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchGuardrails();
|
||||||
|
}, [accessToken]);
|
||||||
|
|
||||||
let metadataString = '';
|
let metadataString = '';
|
||||||
try {
|
try {
|
||||||
metadataString = JSON.stringify(token.metadata, null, 2);
|
// Create a copy of metadata without guardrails for display
|
||||||
|
const displayMetadata = { ...token.metadata };
|
||||||
|
delete displayMetadata.guardrails;
|
||||||
|
metadataString = JSON.stringify(displayMetadata, null, 2);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error stringifying metadata:", error);
|
console.error("Error stringifying metadata:", error);
|
||||||
// You can choose a fallback, such as an empty string or a warning message
|
|
||||||
metadataString = '';
|
metadataString = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure token is defined and handle gracefully if not
|
// Extract existing guardrails from metadata
|
||||||
|
let existingGuardrails: string[] = [];
|
||||||
|
try {
|
||||||
|
existingGuardrails = token.metadata?.guardrails || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error extracting guardrails:", error);
|
||||||
|
}
|
||||||
|
|
||||||
const initialValues = token ? {
|
const initialValues = token ? {
|
||||||
...token,
|
...token,
|
||||||
budget_duration: token.budget_duration,
|
budget_duration: token.budget_duration,
|
||||||
metadata: metadataString
|
metadata: metadataString,
|
||||||
} : { metadata: metadataString };
|
guardrails: existingGuardrails
|
||||||
|
} : { metadata: metadataString, guardrails: [] };
|
||||||
|
|
||||||
|
|
||||||
const handleOk = () => {
|
const handleOk = () => {
|
||||||
|
@ -508,6 +537,33 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
>
|
>
|
||||||
<InputNumber step={1} precision={1} width={200} />
|
<InputNumber step={1} precision={1} width={200} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
Guardrails{' '}
|
||||||
|
<Tooltip title="Setup your first guardrail">
|
||||||
|
<a
|
||||||
|
href="https://docs.litellm.ai/docs/proxy/guardrails/quick_start"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<InfoCircleOutlined style={{ marginLeft: '4px' }} />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
name="guardrails"
|
||||||
|
className="mt-8"
|
||||||
|
help="Select existing guardrails or enter new ones"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
mode="tags"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder="Select or enter guardrails"
|
||||||
|
options={guardrailsList.map(name => ({ value: name, label: name }))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Metadata (ensure this is valid JSON)"
|
label="Metadata (ensure this is valid JSON)"
|
||||||
name="metadata"
|
name="metadata"
|
||||||
|
@ -747,12 +803,23 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
// Convert metadata back to an object if it exists and is a string
|
// Convert metadata back to an object if it exists and is a string
|
||||||
if (formValues.metadata && typeof formValues.metadata === 'string') {
|
if (formValues.metadata && typeof formValues.metadata === 'string') {
|
||||||
try {
|
try {
|
||||||
formValues.metadata = JSON.parse(formValues.metadata);
|
const parsedMetadata = JSON.parse(formValues.metadata);
|
||||||
|
// Only add guardrails if they are set in form values
|
||||||
|
formValues.metadata = {
|
||||||
|
...parsedMetadata,
|
||||||
|
...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {})
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error parsing metadata JSON:", error);
|
console.error("Error parsing metadata JSON:", error);
|
||||||
message.error("Invalid metadata JSON for formValue " + formValues.metadata);
|
message.error("Invalid metadata JSON for formValue " + formValues.metadata);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// If metadata is not a string (or doesn't exist), only add guardrails if they are set
|
||||||
|
formValues.metadata = {
|
||||||
|
...(formValues.metadata || {}),
|
||||||
|
...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {})
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the budget_duration back to the API expected format
|
// Convert the budget_duration back to the API expected format
|
||||||
|
@ -772,6 +839,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
|
|
||||||
console.log("handleEditSubmit:", formValues);
|
console.log("handleEditSubmit:", formValues);
|
||||||
|
|
||||||
|
try {
|
||||||
let newKeyValues = await keyUpdateCall(accessToken, formValues);
|
let newKeyValues = await keyUpdateCall(accessToken, formValues);
|
||||||
console.log("handleEditSubmit: newKeyValues", newKeyValues);
|
console.log("handleEditSubmit: newKeyValues", newKeyValues);
|
||||||
|
|
||||||
|
@ -786,9 +854,14 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
|
|
||||||
setEditModalVisible(false);
|
setEditModalVisible(false);
|
||||||
setSelectedToken(null);
|
setSelectedToken(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating key:", error);
|
||||||
|
message.error("Failed to update key");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleDelete = async (token: any) => {
|
const handleDelete = async (token: any) => {
|
||||||
console.log("handleDelete:", token);
|
console.log("handleDelete:", token);
|
||||||
if (token.token == null) {
|
if (token.token == null) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue