mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-26 03:04:13 +00:00
Litellm dev 01 10 2025 p2 (#7679)
* test(test_basic_python_version.py): assert all optional dependencies are marked as extras on poetry Fixes https://github.com/BerriAI/litellm/issues/7677 * docs(secret.md): clarify 'read_and_write' secret manager usage on aws * docs(secret.md): fix doc * build(ui/teams.tsx): add edit/delete button for updating user / team membership on ui allows updating user role to admin on ui * build(ui/teams.tsx): display edit member component on ui, when edit button on member clicked * feat(team_endpoints.py): support updating team member role to admin via api endpoints allows team member to become admin post-add * build(ui/user_dashboard.tsx): if team admin - show all team keys Fixes https://github.com/BerriAI/litellm/issues/7650 * test(config.yml): add tomli to ci/cd * test: don't call python_basic_testing in local testing (covered by python 3.13 testing)
This commit is contained in:
parent
49d74748b0
commit
c4780479a9
15 changed files with 425 additions and 67 deletions
|
@ -105,7 +105,7 @@ jobs:
|
||||||
command: |
|
command: |
|
||||||
pwd
|
pwd
|
||||||
ls
|
ls
|
||||||
python -m pytest -vv tests/local_testing --cov=litellm --cov-report=xml -x --junitxml=test-results/junit.xml --durations=5 -k "not test_python_38.py and not router and not assistants and not langfuse and not caching and not cache" -n 4
|
python -m pytest -vv tests/local_testing --cov=litellm --cov-report=xml -x --junitxml=test-results/junit.xml --durations=5 -k "not test_python_38.py and not test_basic_python_version.py and not router and not assistants and not langfuse and not caching and not cache" -n 4
|
||||||
no_output_timeout: 120m
|
no_output_timeout: 120m
|
||||||
- run:
|
- run:
|
||||||
name: Rename the coverage files
|
name: Rename the coverage files
|
||||||
|
@ -895,6 +895,7 @@ jobs:
|
||||||
pip install "pytest-retry==1.6.3"
|
pip install "pytest-retry==1.6.3"
|
||||||
pip install "pytest-asyncio==0.21.1"
|
pip install "pytest-asyncio==0.21.1"
|
||||||
pip install "pytest-cov==5.0.0"
|
pip install "pytest-cov==5.0.0"
|
||||||
|
pip install "tomli==2.2.1"
|
||||||
- run:
|
- run:
|
||||||
name: Run tests
|
name: Run tests
|
||||||
command: |
|
command: |
|
||||||
|
|
|
@ -358,7 +358,7 @@ poetry install -E extra_proxy -E proxy
|
||||||
Step 3: Test your change:
|
Step 3: Test your change:
|
||||||
|
|
||||||
```
|
```
|
||||||
cd litellm/tests # pwd: Documents/litellm/litellm/tests
|
cd tests # pwd: Documents/litellm/litellm/tests
|
||||||
poetry run flake8
|
poetry run flake8
|
||||||
poetry run pytest .
|
poetry run pytest .
|
||||||
```
|
```
|
||||||
|
|
BIN
dist/litellm-1.57.6.tar.gz
vendored
Normal file
BIN
dist/litellm-1.57.6.tar.gz
vendored
Normal file
Binary file not shown.
|
@ -72,6 +72,20 @@ general_settings:
|
||||||
prefix_for_stored_virtual_keys: "litellm/" # OPTIONAL. If set, this prefix will be used for stored virtual keys in the secret manager
|
prefix_for_stored_virtual_keys: "litellm/" # OPTIONAL. If set, this prefix will be used for stored virtual keys in the secret manager
|
||||||
access_mode: "write_only" # Literal["read_only", "write_only", "read_and_write"]
|
access_mode: "write_only" # Literal["read_only", "write_only", "read_and_write"]
|
||||||
```
|
```
|
||||||
|
</TabItem>
|
||||||
|
<TabItem value="read_and_write" label="Read + Write Keys with AWS Secret Manager">
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
general_settings:
|
||||||
|
master_key: os.environ/litellm_master_key
|
||||||
|
key_management_system: "aws_secret_manager" # 👈 KEY CHANGE
|
||||||
|
key_management_settings:
|
||||||
|
store_virtual_keys: true # OPTIONAL. Defaults to False, when True will store virtual keys in secret manager
|
||||||
|
prefix_for_stored_virtual_keys: "litellm/" # OPTIONAL. If set, this prefix will be used for stored virtual keys in the secret manager
|
||||||
|
access_mode: "read_and_write" # Literal["read_only", "write_only", "read_and_write"]
|
||||||
|
hosted_keys: ["litellm_master_key"] # OPTIONAL. Specify which env keys you stored on AWS
|
||||||
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
@ -186,34 +200,6 @@ LiteLLM stores secret under the `prefix_for_stored_virtual_keys` path (default:
|
||||||
|
|
||||||
|
|
||||||
## Azure Key Vault
|
## Azure Key Vault
|
||||||
<!--
|
|
||||||
### Quick Start
|
|
||||||
|
|
||||||
```python
|
|
||||||
### Instantiate Azure Key Vault Client ###
|
|
||||||
from azure.keyvault.secrets import SecretClient
|
|
||||||
from azure.identity import ClientSecretCredential
|
|
||||||
|
|
||||||
# Set your Azure Key Vault URI
|
|
||||||
KVUri = os.getenv("AZURE_KEY_VAULT_URI")
|
|
||||||
|
|
||||||
# Set your Azure AD application/client ID, client secret, and tenant ID - create an application with permission to call your key vault
|
|
||||||
client_id = os.getenv("AZURE_CLIENT_ID")
|
|
||||||
client_secret = os.getenv("AZURE_CLIENT_SECRET")
|
|
||||||
tenant_id = os.getenv("AZURE_TENANT_ID")
|
|
||||||
|
|
||||||
# Initialize the ClientSecretCredential
|
|
||||||
credential = ClientSecretCredential(client_id=client_id, client_secret=client_secret, tenant_id=tenant_id)
|
|
||||||
|
|
||||||
# Create the SecretClient using the credential
|
|
||||||
client = SecretClient(vault_url=KVUri, credential=credential)
|
|
||||||
|
|
||||||
### Connect to LiteLLM ###
|
|
||||||
import litellm
|
|
||||||
litellm.secret_manager = client
|
|
||||||
|
|
||||||
litellm.get_secret("your-test-key")
|
|
||||||
``` -->
|
|
||||||
|
|
||||||
#### Usage with LiteLLM Proxy Server
|
#### Usage with LiteLLM Proxy Server
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -2078,12 +2078,13 @@ class TeamMemberDeleteRequest(MemberDeleteRequest):
|
||||||
|
|
||||||
|
|
||||||
class TeamMemberUpdateRequest(TeamMemberDeleteRequest):
|
class TeamMemberUpdateRequest(TeamMemberDeleteRequest):
|
||||||
max_budget_in_team: float
|
max_budget_in_team: Optional[float] = None
|
||||||
|
role: Optional[Literal["admin", "user"]] = None
|
||||||
|
|
||||||
|
|
||||||
class TeamMemberUpdateResponse(MemberUpdateResponse):
|
class TeamMemberUpdateResponse(MemberUpdateResponse):
|
||||||
team_id: str
|
team_id: str
|
||||||
max_budget_in_team: float
|
max_budget_in_team: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
# Organization Member Requests
|
# Organization Member Requests
|
||||||
|
|
|
@ -920,7 +920,7 @@ async def team_member_update(
|
||||||
"""
|
"""
|
||||||
[BETA]
|
[BETA]
|
||||||
|
|
||||||
Update team member budgets
|
Update team member budgets and team member role
|
||||||
"""
|
"""
|
||||||
from litellm.proxy.proxy_server import prisma_client
|
from litellm.proxy.proxy_server import prisma_client
|
||||||
|
|
||||||
|
@ -970,6 +970,8 @@ async def team_member_update(
|
||||||
user_api_key_dict=user_api_key_dict,
|
user_api_key_dict=user_api_key_dict,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
team_table = returned_team_info["team_info"]
|
||||||
|
|
||||||
## get user id
|
## get user id
|
||||||
received_user_id: Optional[str] = None
|
received_user_id: Optional[str] = None
|
||||||
if data.user_id is not None:
|
if data.user_id is not None:
|
||||||
|
@ -995,26 +997,50 @@ async def team_member_update(
|
||||||
break
|
break
|
||||||
|
|
||||||
### upsert new budget
|
### upsert new budget
|
||||||
if identified_budget_id is None:
|
if data.max_budget_in_team is not None:
|
||||||
new_budget = await prisma_client.db.litellm_budgettable.create(
|
if identified_budget_id is None:
|
||||||
data={
|
new_budget = await prisma_client.db.litellm_budgettable.create(
|
||||||
"max_budget": data.max_budget_in_team,
|
data={
|
||||||
"created_by": user_api_key_dict.user_id or "",
|
"max_budget": data.max_budget_in_team,
|
||||||
"updated_by": user_api_key_dict.user_id or "",
|
"created_by": user_api_key_dict.user_id or "",
|
||||||
}
|
"updated_by": user_api_key_dict.user_id or "",
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
|
||||||
await prisma_client.db.litellm_teammembership.create(
|
await prisma_client.db.litellm_teammembership.create(
|
||||||
data={
|
data={
|
||||||
"team_id": data.team_id,
|
"team_id": data.team_id,
|
||||||
"user_id": received_user_id,
|
"user_id": received_user_id,
|
||||||
"budget_id": new_budget.budget_id,
|
"budget_id": new_budget.budget_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
else:
|
elif identified_budget_id is not None:
|
||||||
await prisma_client.db.litellm_budgettable.update(
|
await prisma_client.db.litellm_budgettable.update(
|
||||||
where={"budget_id": identified_budget_id},
|
where={"budget_id": identified_budget_id},
|
||||||
data={"max_budget": data.max_budget_in_team},
|
data={"max_budget": data.max_budget_in_team},
|
||||||
|
)
|
||||||
|
|
||||||
|
### update team member role
|
||||||
|
if data.role is not None:
|
||||||
|
team_members: List[Member] = []
|
||||||
|
for member in team_table.members_with_roles:
|
||||||
|
if member.user_id == received_user_id:
|
||||||
|
team_members.append(
|
||||||
|
Member(
|
||||||
|
user_id=member.user_id,
|
||||||
|
role=data.role,
|
||||||
|
user_email=data.user_email or member.user_email,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
team_members.append(member)
|
||||||
|
|
||||||
|
team_table.members_with_roles = team_members
|
||||||
|
|
||||||
|
_db_team_members: List[dict] = [m.model_dump() for m in team_members]
|
||||||
|
await prisma_client.db.litellm_teamtable.update(
|
||||||
|
where={"team_id": data.team_id},
|
||||||
|
data={"members_with_roles": json.dumps(_db_team_members)}, # type: ignore
|
||||||
)
|
)
|
||||||
|
|
||||||
return TeamMemberUpdateResponse(
|
return TeamMemberUpdateResponse(
|
||||||
|
|
|
@ -37,6 +37,51 @@ def test_litellm_proxy_server():
|
||||||
assert True
|
assert True
|
||||||
|
|
||||||
|
|
||||||
|
def test_package_dependencies():
|
||||||
|
try:
|
||||||
|
import tomli
|
||||||
|
import pathlib
|
||||||
|
import litellm
|
||||||
|
|
||||||
|
# Get the litellm package root path
|
||||||
|
litellm_path = pathlib.Path(litellm.__file__).parent.parent
|
||||||
|
pyproject_path = litellm_path / "pyproject.toml"
|
||||||
|
|
||||||
|
# Read and parse pyproject.toml
|
||||||
|
with open(pyproject_path, "rb") as f:
|
||||||
|
pyproject = tomli.load(f)
|
||||||
|
|
||||||
|
# Get all optional dependencies from poetry.dependencies
|
||||||
|
poetry_deps = pyproject["tool"]["poetry"]["dependencies"]
|
||||||
|
optional_deps = {
|
||||||
|
name.lower()
|
||||||
|
for name, value in poetry_deps.items()
|
||||||
|
if isinstance(value, dict) and value.get("optional", False)
|
||||||
|
}
|
||||||
|
print(optional_deps)
|
||||||
|
# Get all packages listed in extras
|
||||||
|
extras = pyproject["tool"]["poetry"]["extras"]
|
||||||
|
all_extra_deps = set()
|
||||||
|
for extra_group in extras.values():
|
||||||
|
all_extra_deps.update(dep.lower() for dep in extra_group)
|
||||||
|
print(all_extra_deps)
|
||||||
|
# Check that all optional dependencies are in some extras group
|
||||||
|
missing_from_extras = optional_deps - all_extra_deps
|
||||||
|
assert (
|
||||||
|
not missing_from_extras
|
||||||
|
), f"Optional dependencies missing from extras: {missing_from_extras}"
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"All {len(optional_deps)} optional dependencies are correctly specified in extras"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(
|
||||||
|
f"Error occurred while checking dependencies: {str(e)}\n"
|
||||||
|
+ traceback.format_exc()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
|
|
|
@ -2267,6 +2267,47 @@ export const teamMemberAddCall = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const teamMemberUpdateCall = async (
|
||||||
|
accessToken: string,
|
||||||
|
teamId: string,
|
||||||
|
formValues: Member // Assuming formValues is an object
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
console.log("Form Values in teamMemberAddCall:", formValues); // Log the form values before making the API call
|
||||||
|
|
||||||
|
const url = proxyBaseUrl
|
||||||
|
? `${proxyBaseUrl}/team/member_update`
|
||||||
|
: `/team/member_update`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
team_id: teamId,
|
||||||
|
role: formValues.role,
|
||||||
|
user_id: formValues.user_id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.text();
|
||||||
|
handleError(errorData);
|
||||||
|
console.error("Error response from the server:", errorData);
|
||||||
|
throw new Error("Network response was not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log("API Response:", data);
|
||||||
|
return data;
|
||||||
|
// Handle success - you might want to update some state or UI based on the created key
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create key:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const organizationMemberAddCall = async (
|
export const organizationMemberAddCall = async (
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
organizationId: string,
|
organizationId: string,
|
||||||
|
|
139
ui/litellm-dashboard/src/components/team/edit_membership.tsx
Normal file
139
ui/litellm-dashboard/src/components/team/edit_membership.tsx
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Modal, Form, Input, Select as AntSelect, Button as AntButton, message } from 'antd';
|
||||||
|
import { Select, SelectItem } from "@tremor/react";
|
||||||
|
import { Card, Text } from "@tremor/react";
|
||||||
|
|
||||||
|
export interface TeamMember {
|
||||||
|
id?: string;
|
||||||
|
email?: string;
|
||||||
|
role: 'admin' | 'user';
|
||||||
|
team_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TeamMemberModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSubmit: (data: TeamMember) => void;
|
||||||
|
initialData?: TeamMember | null;
|
||||||
|
mode: 'add' | 'edit';
|
||||||
|
}
|
||||||
|
|
||||||
|
const TeamMemberModal: React.FC<TeamMemberModalProps> = ({
|
||||||
|
visible,
|
||||||
|
onCancel,
|
||||||
|
onSubmit,
|
||||||
|
initialData,
|
||||||
|
mode
|
||||||
|
}) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const handleSubmit = async (values: any) => {
|
||||||
|
try {
|
||||||
|
const formData: TeamMember = {
|
||||||
|
email: values.user_email,
|
||||||
|
id: values.user_id,
|
||||||
|
role: values.role
|
||||||
|
};
|
||||||
|
|
||||||
|
onSubmit(formData);
|
||||||
|
form.resetFields();
|
||||||
|
message.success(`Successfully ${mode === 'add' ? 'added' : 'updated'} team member`);
|
||||||
|
} catch (error) {
|
||||||
|
message.error('Failed to submit form');
|
||||||
|
console.error('Form submission error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={mode === 'add' ? "Add Team Member" : "Edit Team Member"}
|
||||||
|
visible={visible}
|
||||||
|
width={800}
|
||||||
|
footer={null}
|
||||||
|
onCancel={onCancel}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
onFinish={handleSubmit}
|
||||||
|
labelCol={{ span: 8 }}
|
||||||
|
wrapperCol={{ span: 16 }}
|
||||||
|
labelAlign="left"
|
||||||
|
initialValues={{
|
||||||
|
user_email: initialData?.email?.trim() || '',
|
||||||
|
user_id: initialData?.id?.trim() || '',
|
||||||
|
role: initialData?.role || 'user',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
label="Email"
|
||||||
|
name="user_email"
|
||||||
|
className="mb-4"
|
||||||
|
rules={[
|
||||||
|
{ type: 'email', message: 'Please enter a valid email!' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="user_email"
|
||||||
|
className="px-3 py-2 border rounded-md w-full"
|
||||||
|
placeholder="user@example.com"
|
||||||
|
onChange={(e) => {
|
||||||
|
e.target.value = e.target.value.trim();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<div className="text-center mb-4">
|
||||||
|
<Text>OR</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="User ID"
|
||||||
|
name="user_id"
|
||||||
|
className="mb-4"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="user_id"
|
||||||
|
className="px-3 py-2 border rounded-md w-full"
|
||||||
|
placeholder="user_123"
|
||||||
|
onChange={(e) => {
|
||||||
|
e.target.value = e.target.value.trim();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label="Member Role"
|
||||||
|
name="role"
|
||||||
|
className="mb-4"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: 'Please select a role!' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<AntSelect defaultValue="user">
|
||||||
|
<AntSelect.Option value="admin">admin</AntSelect.Option>
|
||||||
|
<AntSelect.Option value="user">user</AntSelect.Option>
|
||||||
|
</AntSelect>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<div style={{ textAlign: "right", marginTop: "20px" }}>
|
||||||
|
<AntButton
|
||||||
|
onClick={onCancel}
|
||||||
|
style={{ marginRight: 8 }}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</AntButton>
|
||||||
|
<AntButton
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
>
|
||||||
|
{mode === 'add' ? 'Add Member' : 'Save Changes'}
|
||||||
|
</AntButton>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeamMemberModal;
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Typography } from "antd";
|
import { Typography } from "antd";
|
||||||
import { teamDeleteCall, teamUpdateCall, teamInfoCall } from "./networking";
|
import { teamDeleteCall, teamUpdateCall, teamInfoCall } from "./networking";
|
||||||
|
import TeamMemberModal, { TeamMember } from "@/components/team/edit_membership";
|
||||||
import {
|
import {
|
||||||
InformationCircleIcon,
|
InformationCircleIcon,
|
||||||
PencilAltIcon,
|
PencilAltIcon,
|
||||||
|
@ -65,12 +66,12 @@ interface EditTeamModalProps {
|
||||||
import {
|
import {
|
||||||
teamCreateCall,
|
teamCreateCall,
|
||||||
teamMemberAddCall,
|
teamMemberAddCall,
|
||||||
|
teamMemberUpdateCall,
|
||||||
Member,
|
Member,
|
||||||
modelAvailableCall,
|
modelAvailableCall,
|
||||||
teamListCall
|
teamListCall
|
||||||
} from "./networking";
|
} from "./networking";
|
||||||
|
|
||||||
|
|
||||||
const Team: React.FC<TeamProps> = ({
|
const Team: React.FC<TeamProps> = ({
|
||||||
teams,
|
teams,
|
||||||
searchParams,
|
searchParams,
|
||||||
|
@ -112,11 +113,12 @@ const Team: React.FC<TeamProps> = ({
|
||||||
|
|
||||||
const [isTeamModalVisible, setIsTeamModalVisible] = useState(false);
|
const [isTeamModalVisible, setIsTeamModalVisible] = useState(false);
|
||||||
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
|
const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false);
|
||||||
|
const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false);
|
||||||
const [userModels, setUserModels] = useState([]);
|
const [userModels, setUserModels] = useState([]);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [teamToDelete, setTeamToDelete] = useState<string | null>(null);
|
const [teamToDelete, setTeamToDelete] = useState<string | null>(null);
|
||||||
|
const [selectedEditMember, setSelectedEditMember] = useState<null | TeamMember>(null);
|
||||||
|
|
||||||
// store team info as {"team_id": team_info_object}
|
|
||||||
const [perTeamInfo, setPerTeamInfo] = useState<Record<string, any>>({});
|
const [perTeamInfo, setPerTeamInfo] = useState<Record<string, any>>({});
|
||||||
|
|
||||||
const EditTeamModal: React.FC<EditTeamModalProps> = ({
|
const EditTeamModal: React.FC<EditTeamModalProps> = ({
|
||||||
|
@ -257,16 +259,19 @@ const Team: React.FC<TeamProps> = ({
|
||||||
|
|
||||||
const handleMemberOk = () => {
|
const handleMemberOk = () => {
|
||||||
setIsAddMemberModalVisible(false);
|
setIsAddMemberModalVisible(false);
|
||||||
|
setIsEditMemberModalVisible(false);
|
||||||
memberForm.resetFields();
|
memberForm.resetFields();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setIsTeamModalVisible(false);
|
setIsTeamModalVisible(false);
|
||||||
|
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMemberCancel = () => {
|
const handleMemberCancel = () => {
|
||||||
setIsAddMemberModalVisible(false);
|
setIsAddMemberModalVisible(false);
|
||||||
|
setIsEditMemberModalVisible(false);
|
||||||
memberForm.resetFields();
|
memberForm.resetFields();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -412,7 +417,7 @@ const Team: React.FC<TeamProps> = ({
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMemberCreate = async (formValues: Record<string, any>) => {
|
const _common_member_update_call = async (formValues: Record<string, any>, callType: "add" | "edit") => {
|
||||||
try {
|
try {
|
||||||
if (accessToken != null && teams != null) {
|
if (accessToken != null && teams != null) {
|
||||||
message.info("Adding Member");
|
message.info("Adding Member");
|
||||||
|
@ -421,13 +426,27 @@ const Team: React.FC<TeamProps> = ({
|
||||||
user_email: formValues.user_email,
|
user_email: formValues.user_email,
|
||||||
user_id: formValues.user_id,
|
user_id: formValues.user_id,
|
||||||
};
|
};
|
||||||
const response: any = await teamMemberAddCall(
|
let response: any;
|
||||||
accessToken,
|
if (callType == "add") {
|
||||||
selectedTeam["team_id"],
|
response = await teamMemberAddCall(
|
||||||
user_role
|
accessToken,
|
||||||
);
|
selectedTeam["team_id"],
|
||||||
message.success("Member added");
|
user_role
|
||||||
console.log(`response for team create call: ${response["data"]}`);
|
);
|
||||||
|
message.success("Member added");
|
||||||
|
} else {
|
||||||
|
response = await teamMemberUpdateCall(
|
||||||
|
accessToken,
|
||||||
|
selectedTeam["team_id"],
|
||||||
|
{
|
||||||
|
"role": formValues.role,
|
||||||
|
"user_id": formValues.id,
|
||||||
|
"user_email": formValues.email
|
||||||
|
}
|
||||||
|
);
|
||||||
|
message.success("Member updated");
|
||||||
|
}
|
||||||
|
|
||||||
// Checking if the team exists in the list and updating or adding accordingly
|
// Checking if the team exists in the list and updating or adding accordingly
|
||||||
const foundIndex = teams.findIndex((team) => {
|
const foundIndex = teams.findIndex((team) => {
|
||||||
console.log(
|
console.log(
|
||||||
|
@ -449,7 +468,15 @@ const Team: React.FC<TeamProps> = ({
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating the team:", error);
|
console.error("Error creating the team:", error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMemberCreate = async (formValues: Record<string, any>) => {
|
||||||
|
_common_member_update_call(formValues, "add");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleMemberUpdate = async (formValues: Record<string, any>) => {
|
||||||
|
_common_member_update_call(formValues, "edit");
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="w-full mx-4">
|
<div className="w-full mx-4">
|
||||||
<Grid numItems={1} className="gap-2 p-8 h-[75vh] w-full mt-2">
|
<Grid numItems={1} className="gap-2 p-8 h-[75vh] w-full mt-2">
|
||||||
|
@ -831,6 +858,30 @@ const Team: React.FC<TeamProps> = ({
|
||||||
: null}
|
: null}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{member["role"]}</TableCell>
|
<TableCell>{member["role"]}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{userRole == "Admin" ? (
|
||||||
|
<>
|
||||||
|
<Icon
|
||||||
|
icon={PencilAltIcon}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditMemberModalVisible(true);
|
||||||
|
setSelectedEditMember({
|
||||||
|
"id": member["user_id"],
|
||||||
|
"email": member["user_email"],
|
||||||
|
"team_id": selectedTeam["team_id"],
|
||||||
|
"role": member["role"]
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
onClick={() => {}}
|
||||||
|
icon={TrashIcon}
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -838,6 +889,13 @@ const Team: React.FC<TeamProps> = ({
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</Card>
|
</Card>
|
||||||
|
<TeamMemberModal
|
||||||
|
visible={isEditMemberModalVisible}
|
||||||
|
onCancel={handleMemberCancel}
|
||||||
|
onSubmit={handleMemberUpdate}
|
||||||
|
initialData={selectedEditMember}
|
||||||
|
mode="edit"
|
||||||
|
/>
|
||||||
{selectedTeam && (
|
{selectedTeam && (
|
||||||
<EditTeamModal
|
<EditTeamModal
|
||||||
visible={editModalVisible}
|
visible={editModalVisible}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
modelAvailableCall,
|
modelAvailableCall,
|
||||||
getTotalSpendCall,
|
getTotalSpendCall,
|
||||||
getProxyUISettings,
|
getProxyUISettings,
|
||||||
|
teamListCall,
|
||||||
} from "./networking";
|
} from "./networking";
|
||||||
import { Grid, Col, Card, Text, Title } from "@tremor/react";
|
import { Grid, Col, Card, Text, Title } from "@tremor/react";
|
||||||
import CreateKey from "./create_key_button";
|
import CreateKey from "./create_key_button";
|
||||||
|
@ -172,6 +173,18 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||||
if (cachedUserModels) {
|
if (cachedUserModels) {
|
||||||
setUserModels(JSON.parse(cachedUserModels));
|
setUserModels(JSON.parse(cachedUserModels));
|
||||||
} else {
|
} else {
|
||||||
|
const fetchTeams = async () => {
|
||||||
|
let givenTeams;
|
||||||
|
if (userRole != "Admin" && userRole != "Admin Viewer") {
|
||||||
|
givenTeams = await teamListCall(accessToken, userID)
|
||||||
|
} else {
|
||||||
|
givenTeams = await teamListCall(accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`givenTeams: ${givenTeams}`)
|
||||||
|
|
||||||
|
setTeams(givenTeams)
|
||||||
|
}
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const proxy_settings: ProxySettings = await getProxyUISettings(accessToken);
|
const proxy_settings: ProxySettings = await getProxyUISettings(accessToken);
|
||||||
|
@ -194,7 +207,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)}`)
|
||||||
setKeys(response["keys"]); // Assuming this is the correct path to your data
|
setKeys(response["keys"]); // Assuming this is the correct path to your data
|
||||||
setTeams(response["teams"]);
|
|
||||||
const teamsArray = [...response["teams"]];
|
const teamsArray = [...response["teams"]];
|
||||||
if (teamsArray.length > 0) {
|
if (teamsArray.length > 0) {
|
||||||
console.log(`response['teams']: ${teamsArray}`);
|
console.log(`response['teams']: ${teamsArray}`);
|
||||||
|
@ -235,6 +247,7 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchData();
|
fetchData();
|
||||||
|
fetchTeams();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [userID, token, accessToken, keys, userRole]);
|
}, [userID, token, accessToken, keys, userRole]);
|
||||||
|
|
|
@ -131,6 +131,57 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
|
|
||||||
const [knownTeamIDs, setKnownTeamIDs] = useState(initialKnownTeamIDs);
|
const [knownTeamIDs, setKnownTeamIDs] = useState(initialKnownTeamIDs);
|
||||||
|
|
||||||
|
// Function to check if user is admin of a team
|
||||||
|
const isUserTeamAdmin = (team: any) => {
|
||||||
|
if (!team.members_with_roles) return false;
|
||||||
|
return team.members_with_roles.some(
|
||||||
|
(member: any) => member.role === "admin" && member.user_id === userID
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Combine all keys that user should have access to
|
||||||
|
const all_keys_to_display = React.useMemo(() => {
|
||||||
|
let allKeys: any[] = [];
|
||||||
|
|
||||||
|
// If no teams, return personal keys
|
||||||
|
if (!teams || teams.length === 0) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
teams.forEach(team => {
|
||||||
|
// For default team or when user is not admin, use personal keys (data)
|
||||||
|
if (team.team_id === "default-team" || !isUserTeamAdmin(team)) {
|
||||||
|
if (selectedTeam && selectedTeam.team_id === team.team_id) {
|
||||||
|
allKeys = [...allKeys, ...data.filter(key => key.team_id === team.team_id)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For teams where user is admin, use team keys
|
||||||
|
else if (isUserTeamAdmin(team)) {
|
||||||
|
if (selectedTeam && selectedTeam.team_id === team.team_id) {
|
||||||
|
allKeys = [...allKeys, ...(team.keys || [])];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no team is selected, show all accessible keys
|
||||||
|
if (!selectedTeam) {
|
||||||
|
const personalKeys = data.filter(key => !key.team_id || key.team_id === "default-team");
|
||||||
|
const adminTeamKeys = teams
|
||||||
|
.filter(team => isUserTeamAdmin(team))
|
||||||
|
.flatMap(team => team.keys || []);
|
||||||
|
allKeys = [...personalKeys, ...adminTeamKeys];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out litellm-dashboard keys
|
||||||
|
allKeys = allKeys.filter(key => key.team_id !== "litellm-dashboard");
|
||||||
|
|
||||||
|
// Remove duplicates based on token
|
||||||
|
const uniqueKeys = Array.from(
|
||||||
|
new Map(allKeys.map(key => [key.token, key])).values()
|
||||||
|
);
|
||||||
|
|
||||||
|
return uniqueKeys;
|
||||||
|
}, [data, teams, selectedTeam, userID]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const calculateNewExpiryTime = (duration: string | undefined) => {
|
const calculateNewExpiryTime = (duration: string | undefined) => {
|
||||||
|
@ -858,7 +909,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{data.map((item) => {
|
{all_keys_to_display.map((item) => {
|
||||||
console.log(item);
|
console.log(item);
|
||||||
// skip item if item.team_id == "litellm-dashboard"
|
// skip item if item.team_id == "litellm-dashboard"
|
||||||
if (item.team_id === "litellm-dashboard") {
|
if (item.team_id === "litellm-dashboard") {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue