mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-27 03:34:10 +00:00
refactor(teams.tsx): refactor to display all teams, across all orgs (#8565)
* refactor(teams.tsx): refactor to display all teams, across all orgs
removes org switcher from navbar, simplifies viewing/creating teams on UI
* fix(key_list.tsx): show user keys across all orgs
make it easy to see flat list of keys across orgs on key table
* style(all_keys_table.tsx): cleanup keys table
* fix(user_dashboard.tsx): remove overflow-hidden in dashboard component
* fix(teams.tsx): move org id placement in create team flow
* fix(teams.tsx): support model selection on create team based on selected org
* feat(view_key_table.tsx): move to using a filter component on keys page
allows filtering keys by org and team
* fix(filter.tsx): handle reset filter
* fix: fix linting error
* (Feat) - return `x-litellm-attempted-fallbacks` in responses from litellm proxy (#8558)
* add_fallback_headers_to_response
* test x-litellm-attempted-fallbacks
* unit test attempted fallbacks
* fix add_fallback_headers_to_response
* docs document response headers
* fix file name
* test fix use mock endpoints for e2e files and ft tests
* Revert "test fix use mock endpoints for e2e files and ft tests"
This reverts commit c921d8dd81
.
* cleanup_azure_files
* Add remaining org CRUD endpoints + support deleting orgs on UI (#8561)
* feat(organization_endpoints.py): expose new `/organization/delete` endpoint. Cascade org deletion to member, teams and keys
Ensures any org deletion is handled correctly
* test(test_organizations.py): add simple test to ensure org deletion works
* feat(organization_endpoints.py): expose /organization/update endpoint, and define response models for org delete + update
* fix(organizations.tsx): support org delete on UI + move org/delete endpoint to use DELETE
* feat(organization_endpoints.py): support `/organization/member_update` endpoint
Allow admin to update member's role within org
* feat(organization_endpoints.py): support deleting member from org
* test(test_organizations.py): add e2e test to ensure org member flow works
* fix(organization_endpoints.py): fix code qa check
* fix(schema.prisma): don't introduce ondelete:cascade - breaking change
* docs(organization_endpoints.py): document missing params
* refactor(organization_view.tsx): initial commit creating a generic update member component shared between org and team member classes
* feat(organization_view.tsx): support updating org member role on UI
* feat(organization_view.tsx): allow proxy admin to delete members from org
* Enable update/delete org members on UI (#8560)
* feat(organization_endpoints.py): expose new `/organization/delete` endpoint. Cascade org deletion to member, teams and keys
Ensures any org deletion is handled correctly
* test(test_organizations.py): add simple test to ensure org deletion works
* feat(organization_endpoints.py): expose /organization/update endpoint, and define response models for org delete + update
* fix(organizations.tsx): support org delete on UI + move org/delete endpoint to use DELETE
* feat(organization_endpoints.py): support `/organization/member_update` endpoint
Allow admin to update member's role within org
* feat(organization_endpoints.py): support deleting member from org
* test(test_organizations.py): add e2e test to ensure org member flow works
* fix(organization_endpoints.py): fix code qa check
* fix(schema.prisma): don't introduce ondelete:cascade - breaking change
* docs(organization_endpoints.py): document missing params
* (Bug Fix) - Add Regenerate Key on Virtual Keys Tab (#8567)
* add regenerate key to ui
* ui fix key info
* (Bug Fix + Better Observability) - BudgetResetJob: (#8562)
* use class ResetBudgetJob
* refactor reset budget job
* update reset_budget job
* refactor reset budget job
* fix LiteLLM_UserTable
* refactor reset budget job
* add telemetry for reset budget job
* dd - log service success/failure on DD
* add detailed reset budget reset info on DD
* initialize_scheduled_background_jobs
* refactor reset budget job
* trigger service failure hook when fails to reset a budget for team, key, user
* fix resetBudgetJob
* unit testing for ResetBudgetJob
* test_duration_in_seconds_basic
* testing for triggering service logging
* fix logs on test teams fail
* remove unused imports
* fix import duration in s
* duration_in_seconds
* (Patch/bug fix) - UI, filter out litellm ui session tokens on Virtual Keys Page (#8568)
* fix key list endpoint
* _get_condition_to_filter_out_ui_session_tokens
* duration_in_seconds
* test_list_key_helper_team_filtering
* bump: version 1.61.4 → 1.61.5
* ui fix tsx linting
* ui new build
* test_list_key_helper_team_filtering
* ui new build
* test_openai_fine_tuning
* test_openai_fine_tuning
---------
Co-authored-by: Ishaan Jaff <ishaanjaffer0324@gmail.com>
This commit is contained in:
parent
8923304e85
commit
bdc1a72542
12 changed files with 352 additions and 632 deletions
|
@ -105,6 +105,8 @@ interface ViewKeyTableProps {
|
|||
teams: Team[] | null;
|
||||
premiumUser: boolean;
|
||||
currentOrg: Organization | null;
|
||||
organizations: Organization[] | null;
|
||||
setCurrentOrg: React.Dispatch<React.SetStateAction<Organization | null>>;
|
||||
}
|
||||
|
||||
interface ItemData {
|
||||
|
@ -150,7 +152,9 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
setData,
|
||||
teams,
|
||||
premiumUser,
|
||||
currentOrg
|
||||
currentOrg,
|
||||
organizations,
|
||||
setCurrentOrg
|
||||
}) => {
|
||||
const [isButtonClicked, setIsButtonClicked] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
@ -170,22 +174,6 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
}, [selectedTeam]);
|
||||
|
||||
// Build a memoized filters object for the backend call.
|
||||
const filters = useMemo(
|
||||
() => {
|
||||
const f: { team_id?: string; key_alias?: string } = {};
|
||||
|
||||
if (teamFilter) {
|
||||
f.team_id = teamFilter;
|
||||
}
|
||||
|
||||
if (keyAliasFilter) {
|
||||
f.key_alias = keyAliasFilter;
|
||||
}
|
||||
|
||||
return f;
|
||||
},
|
||||
[teamFilter, keyAliasFilter]
|
||||
);
|
||||
|
||||
// Pass filters into the hook so the API call includes these query parameters.
|
||||
const { keys, isLoading, error, pagination, refresh } = useKeyList({
|
||||
|
@ -330,330 +318,6 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
}
|
||||
}, [teams]);
|
||||
|
||||
|
||||
const ModelLimitModal: React.FC<ModelLimitModalProps> = ({
|
||||
visible,
|
||||
onCancel,
|
||||
token,
|
||||
onSubmit,
|
||||
accessToken,
|
||||
}) => {
|
||||
const [modelLimits, setModelLimits] = useState<{
|
||||
[key: string]: { tpm: number; rpm: number };
|
||||
}>({});
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
const [newModelRow, setNewModelRow] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (token.metadata) {
|
||||
const tpmLimits = token.metadata.model_tpm_limit || {};
|
||||
const rpmLimits = token.metadata.model_rpm_limit || {};
|
||||
const combinedLimits: CombinedLimits = {};
|
||||
|
||||
Object.keys({ ...tpmLimits, ...rpmLimits }).forEach((model) => {
|
||||
combinedLimits[model] = {
|
||||
tpm: (tpmLimits as ModelLimits)[model] || 0,
|
||||
rpm: (rpmLimits as ModelLimits)[model] || 0,
|
||||
};
|
||||
});
|
||||
|
||||
setModelLimits(combinedLimits);
|
||||
}
|
||||
|
||||
const fetchAvailableModels = async () => {
|
||||
try {
|
||||
const modelDataResponse = await modelInfoCall(accessToken, "", "");
|
||||
const allModelGroups: string[] = Array.from(
|
||||
new Set(
|
||||
modelDataResponse.data.map((model: any) => model.model_name)
|
||||
)
|
||||
);
|
||||
setAvailableModels(allModelGroups);
|
||||
} catch (error) {
|
||||
console.error("Error fetching model data:", error);
|
||||
message.error("Failed to fetch available models");
|
||||
}
|
||||
};
|
||||
|
||||
fetchAvailableModels();
|
||||
}, [token, accessToken]);
|
||||
|
||||
const handleLimitChange = (
|
||||
model: string,
|
||||
type: "tpm" | "rpm",
|
||||
value: number | null
|
||||
) => {
|
||||
setModelLimits((prev) => ({
|
||||
...prev,
|
||||
[model]: {
|
||||
...prev[model],
|
||||
[type]: value || 0,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddLimit = () => {
|
||||
setNewModelRow("");
|
||||
};
|
||||
|
||||
const handleModelSelect = (model: string) => {
|
||||
if (!modelLimits[model]) {
|
||||
setModelLimits((prev) => ({
|
||||
...prev,
|
||||
[model]: { tpm: 0, rpm: 0 },
|
||||
}));
|
||||
}
|
||||
setNewModelRow(null);
|
||||
};
|
||||
|
||||
const handleRemoveModel = (model: string) => {
|
||||
setModelLimits((prev) => {
|
||||
const { [model]: _, ...rest } = prev;
|
||||
return rest;
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const updatedMetadata = {
|
||||
...token.metadata,
|
||||
model_tpm_limit: Object.fromEntries(
|
||||
Object.entries(modelLimits).map(([model, limits]) => [
|
||||
model,
|
||||
limits.tpm,
|
||||
])
|
||||
),
|
||||
model_rpm_limit: Object.fromEntries(
|
||||
Object.entries(modelLimits).map(([model, limits]) => [
|
||||
model,
|
||||
limits.rpm,
|
||||
])
|
||||
),
|
||||
};
|
||||
onSubmit(updatedMetadata);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Edit Model-Specific Limits"
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Model</TableHeaderCell>
|
||||
<TableHeaderCell>TPM Limit</TableHeaderCell>
|
||||
<TableHeaderCell>RPM Limit</TableHeaderCell>
|
||||
<TableHeaderCell>Actions</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.entries(modelLimits).map(([model, limits]) => (
|
||||
<TableRow key={model}>
|
||||
<TableCell>{model}</TableCell>
|
||||
<TableCell>
|
||||
<InputNumber
|
||||
value={limits.tpm}
|
||||
onChange={(value) =>
|
||||
handleLimitChange(model, "tpm", value)
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<InputNumber
|
||||
value={limits.rpm}
|
||||
onChange={(value) =>
|
||||
handleLimitChange(model, "rpm", value)
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button onClick={() => handleRemoveModel(model)}>
|
||||
Remove
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{newModelRow !== null && (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Select2
|
||||
style={{ width: 200 }}
|
||||
placeholder="Select a model"
|
||||
onChange={handleModelSelect}
|
||||
value={newModelRow || undefined}
|
||||
>
|
||||
{availableModels
|
||||
.filter((m) => !modelLimits.hasOwnProperty(m))
|
||||
.map((m) => (
|
||||
<Select2.Option key={m} value={m}>
|
||||
{m}
|
||||
</Select2.Option>
|
||||
))}
|
||||
</Select2>
|
||||
</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>-</TableCell>
|
||||
<TableCell>
|
||||
<Button onClick={() => setNewModelRow(null)}>Cancel</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Button onClick={handleAddLimit} disabled={newModelRow !== null}>
|
||||
Add Limit
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-end space-x-4 mt-6">
|
||||
<Button onClick={onCancel}>Cancel</Button>
|
||||
<Button onClick={handleSubmit}>Save</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const handleEditClick = (token: any) => {
|
||||
console.log("handleEditClick:", token);
|
||||
|
||||
// set token.token to token.token_id if token_id is not null
|
||||
if (token.token == null) {
|
||||
if (token.token_id !== null) {
|
||||
token.token = token.token_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the budget_duration to the corresponding select option
|
||||
let budgetDuration = null;
|
||||
if (token.budget_duration) {
|
||||
switch (token.budget_duration) {
|
||||
case "24h":
|
||||
budgetDuration = "daily";
|
||||
break;
|
||||
case "7d":
|
||||
budgetDuration = "weekly";
|
||||
break;
|
||||
case "30d":
|
||||
budgetDuration = "monthly";
|
||||
break;
|
||||
default:
|
||||
budgetDuration = "None";
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedToken({
|
||||
...token,
|
||||
budget_duration: budgetDuration,
|
||||
});
|
||||
|
||||
//setSelectedToken(token);
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEditCancel = () => {
|
||||
setEditModalVisible(false);
|
||||
setSelectedToken(null);
|
||||
};
|
||||
|
||||
const handleEditSubmit = async (formValues: Record<string, any>) => {
|
||||
/**
|
||||
* Call API to update team with teamId and values
|
||||
*
|
||||
* Client-side validation: For selected team, ensure models in team + max budget < team max budget
|
||||
*/
|
||||
if (accessToken == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentKey = formValues.token;
|
||||
formValues.key = currentKey;
|
||||
|
||||
// Convert metadata back to an object if it exists and is a string
|
||||
if (formValues.metadata && typeof formValues.metadata === "string") {
|
||||
try {
|
||||
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) {
|
||||
console.error("Error parsing metadata JSON:", error);
|
||||
message.error(
|
||||
"Invalid metadata JSON for formValue " + formValues.metadata
|
||||
);
|
||||
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
|
||||
if (formValues.budget_duration) {
|
||||
switch (formValues.budget_duration) {
|
||||
case "daily":
|
||||
formValues.budget_duration = "24h";
|
||||
break;
|
||||
case "weekly":
|
||||
formValues.budget_duration = "7d";
|
||||
break;
|
||||
case "monthly":
|
||||
formValues.budget_duration = "30d";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("handleEditSubmit:", formValues);
|
||||
|
||||
try {
|
||||
let newKeyValues = await keyUpdateCall(accessToken, formValues);
|
||||
console.log("handleEditSubmit: newKeyValues", newKeyValues);
|
||||
|
||||
// Update the keys with the update key
|
||||
if (data) {
|
||||
const updatedData = data.map((key) =>
|
||||
key.token === currentKey ? newKeyValues : key
|
||||
);
|
||||
setData(updatedData);
|
||||
}
|
||||
message.success("Key updated successfully");
|
||||
|
||||
setEditModalVisible(false);
|
||||
setSelectedToken(null);
|
||||
} catch (error) {
|
||||
console.error("Error updating key:", error);
|
||||
message.error("Failed to update key");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (token: any) => {
|
||||
console.log("handleDelete:", token);
|
||||
if (token.token == null) {
|
||||
if (token.token_id !== null) {
|
||||
token.token = token.token_id;
|
||||
}
|
||||
}
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the key to delete and open the confirmation modal
|
||||
setKeyToDelete(token.token);
|
||||
localStorage.removeItem("userData" + userID);
|
||||
setIsDeleteModalOpen(true);
|
||||
};
|
||||
|
||||
const confirmDelete = async () => {
|
||||
if (keyToDelete == null || data == null) {
|
||||
return;
|
||||
|
@ -740,11 +404,6 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
// New filter UI rendered above the table.
|
||||
// For the team filter we use the teams prop, and for key alias we compute unique aliases from the keys.
|
||||
const uniqueKeyAliases = Array.from(
|
||||
new Set(keys.map((k) => (k.key_alias ? k.key_alias : "Not Set")))
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -760,6 +419,8 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
|||
accessToken={accessToken}
|
||||
userID={userID}
|
||||
userRole={userRole}
|
||||
organizations={organizations}
|
||||
setCurrentOrg={setCurrentOrg}
|
||||
/>
|
||||
|
||||
{isDeleteModalOpen && (
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue