mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-24 18:24:20 +00:00
[Feat - UI] - Allow setting Default Team setting when LiteLLM SSO auto creates teams (#9918)
* endpoint for updating default team settings on ui * add GET default team settings endpoint * ui expose default team settings on UI * update to use DefaultTeamSSOParams * DefaultTeamSSOParams * fix DefaultTeamSSOParams * docs team management * test_update_default_team_settings
This commit is contained in:
parent
c4ea1ab61b
commit
8b1d2d6956
10 changed files with 646 additions and 61 deletions
|
@ -65,8 +65,8 @@ from litellm.proxy._types import (
|
|||
KeyManagementSystem,
|
||||
KeyManagementSettings,
|
||||
LiteLLM_UpperboundKeyGenerateParams,
|
||||
NewTeamRequest,
|
||||
)
|
||||
from litellm.types.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams
|
||||
from litellm.types.utils import StandardKeyGenerationConfig, LlmProviders
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
from litellm.litellm_core_utils.logging_callback_manager import LoggingCallbackManager
|
||||
|
@ -277,7 +277,7 @@ default_key_generate_params: Optional[Dict] = None
|
|||
upperbound_key_generate_params: Optional[LiteLLM_UpperboundKeyGenerateParams] = None
|
||||
key_generation_settings: Optional[StandardKeyGenerationConfig] = None
|
||||
default_internal_user_params: Optional[Dict] = None
|
||||
default_team_params: Optional[Union[NewTeamRequest, Dict]] = None
|
||||
default_team_params: Optional[Union[DefaultTeamSSOParams, Dict]] = None
|
||||
default_team_settings: Optional[List] = None
|
||||
max_user_budget: Optional[float] = None
|
||||
default_max_internal_user_budget: Optional[float] = None
|
||||
|
|
|
@ -941,7 +941,7 @@ class SSOAuthenticationHandler:
|
|||
|
||||
@staticmethod
|
||||
def _cast_and_deepcopy_litellm_default_team_params(
|
||||
default_team_params: Union[NewTeamRequest, Dict],
|
||||
default_team_params: Union[DefaultTeamSSOParams, Dict],
|
||||
team_request: NewTeamRequest,
|
||||
litellm_team_id: str,
|
||||
litellm_team_name: Optional[str] = None,
|
||||
|
@ -949,23 +949,20 @@ class SSOAuthenticationHandler:
|
|||
"""
|
||||
Casts and deepcopies the litellm.default_team_params to a NewTeamRequest object
|
||||
|
||||
- Ensures we create a new NewTeamRequest object
|
||||
- Handle the case where litellm.default_team_params is a dict or a NewTeamRequest object
|
||||
- Adds the litellm_team_id and litellm_team_name to the NewTeamRequest object
|
||||
- Ensures we create a new DefaultTeamSSOParams object
|
||||
- Handle the case where litellm.default_team_params is a dict or a DefaultTeamSSOParams object
|
||||
- Adds the litellm_team_id and litellm_team_name to the DefaultTeamSSOParams object
|
||||
"""
|
||||
if isinstance(default_team_params, dict):
|
||||
_team_request = deepcopy(default_team_params)
|
||||
_team_request["team_id"] = litellm_team_id
|
||||
_team_request["team_alias"] = litellm_team_name
|
||||
team_request = NewTeamRequest(**_team_request)
|
||||
elif isinstance(litellm.default_team_params, NewTeamRequest):
|
||||
team_request = litellm.default_team_params.model_copy(
|
||||
deep=True,
|
||||
update={
|
||||
"team_id": litellm_team_id,
|
||||
"team_alias": litellm_team_name,
|
||||
},
|
||||
)
|
||||
elif isinstance(litellm.default_team_params, DefaultTeamSSOParams):
|
||||
_default_team_params = deepcopy(litellm.default_team_params)
|
||||
_new_team_request = team_request.model_dump()
|
||||
_new_team_request.update(_default_team_params)
|
||||
team_request = NewTeamRequest(**_new_team_request)
|
||||
return team_request
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#### CRUD ENDPOINTS for UI Settings #####
|
||||
from typing import List
|
||||
from typing import Any, List, Union
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
|
@ -7,6 +7,7 @@ import litellm
|
|||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.proxy._types import *
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.types.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
@ -111,34 +112,31 @@ async def delete_allowed_ip(ip_address: IPAddress):
|
|||
return {"message": f"IP {ip_address.ip} deleted successfully", "status": "success"}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/get/internal_user_settings",
|
||||
tags=["SSO Settings"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def get_sso_settings():
|
||||
async def _get_settings_with_schema(
|
||||
settings_key: str,
|
||||
settings_class: Any,
|
||||
config: dict,
|
||||
) -> dict:
|
||||
"""
|
||||
Get all SSO settings from the litellm_settings configuration.
|
||||
Returns a structured object with values and descriptions for UI display.
|
||||
Common utility function to get settings with schema information.
|
||||
|
||||
Args:
|
||||
settings_key: The key in litellm_settings to get
|
||||
settings_class: The Pydantic class to use for schema
|
||||
config: The config dictionary
|
||||
"""
|
||||
from pydantic import TypeAdapter
|
||||
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
# Load existing config
|
||||
config = await proxy_config.get_config()
|
||||
litellm_settings = config.get("litellm_settings", {}) or {}
|
||||
default_internal_user_params = (
|
||||
litellm_settings.get("default_internal_user_params", {}) or {}
|
||||
)
|
||||
settings_data = litellm_settings.get(settings_key, {}) or {}
|
||||
|
||||
# Create the settings object first
|
||||
sso_settings = DefaultInternalUserParams(**(default_internal_user_params))
|
||||
# Get the schema for UISSOSettings
|
||||
schema = TypeAdapter(DefaultInternalUserParams).json_schema(by_alias=True)
|
||||
# Create the settings object
|
||||
settings = settings_class(**(settings_data))
|
||||
# Get the schema
|
||||
schema = TypeAdapter(settings_class).json_schema(by_alias=True)
|
||||
|
||||
# Convert to dict for response
|
||||
settings_dict = sso_settings.model_dump()
|
||||
settings_dict = settings.model_dump()
|
||||
|
||||
# Add descriptions to the response
|
||||
result = {
|
||||
|
@ -166,6 +164,89 @@ async def get_sso_settings():
|
|||
return result
|
||||
|
||||
|
||||
@router.get(
|
||||
"/get/internal_user_settings",
|
||||
tags=["SSO Settings"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def get_sso_settings():
|
||||
"""
|
||||
Get all SSO settings from the litellm_settings configuration.
|
||||
Returns a structured object with values and descriptions for UI display.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
# Load existing config
|
||||
config = await proxy_config.get_config()
|
||||
|
||||
return await _get_settings_with_schema(
|
||||
settings_key="default_internal_user_params",
|
||||
settings_class=DefaultInternalUserParams,
|
||||
config=config,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/get/default_team_settings",
|
||||
tags=["SSO Settings"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def get_default_team_settings():
|
||||
"""
|
||||
Get all SSO settings from the litellm_settings configuration.
|
||||
Returns a structured object with values and descriptions for UI display.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
# Load existing config
|
||||
config = await proxy_config.get_config()
|
||||
|
||||
return await _get_settings_with_schema(
|
||||
settings_key="default_team_params",
|
||||
settings_class=DefaultTeamSSOParams,
|
||||
config=config,
|
||||
)
|
||||
|
||||
|
||||
async def _update_litellm_setting(
|
||||
settings: Union[DefaultInternalUserParams, DefaultTeamSSOParams],
|
||||
settings_key: str,
|
||||
in_memory_var: Any,
|
||||
success_message: str,
|
||||
):
|
||||
"""
|
||||
Common utility function to update `litellm_settings` in both memory and config.
|
||||
|
||||
Args:
|
||||
settings: The settings object to update
|
||||
settings_key: The key in litellm_settings to update
|
||||
in_memory_var: The in-memory variable to update
|
||||
success_message: Message to return on success
|
||||
"""
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
# Update the in-memory settings
|
||||
in_memory_var = settings.model_dump(exclude_none=True)
|
||||
|
||||
# Load existing config
|
||||
config = await proxy_config.get_config()
|
||||
|
||||
# Update config with new settings
|
||||
if "litellm_settings" not in config:
|
||||
config["litellm_settings"] = {}
|
||||
|
||||
config["litellm_settings"][settings_key] = settings.model_dump(exclude_none=True)
|
||||
|
||||
# Save the updated config
|
||||
await proxy_config.save_config(new_config=config)
|
||||
|
||||
return {
|
||||
"message": success_message,
|
||||
"status": "success",
|
||||
"settings": in_memory_var,
|
||||
}
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/update/internal_user_settings",
|
||||
tags=["SSO Settings"],
|
||||
|
@ -176,27 +257,27 @@ async def update_internal_user_settings(settings: DefaultInternalUserParams):
|
|||
Update the default internal user parameters for SSO users.
|
||||
These settings will be applied to new users who sign in via SSO.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import proxy_config
|
||||
|
||||
# Update the in-memory settings
|
||||
litellm.default_internal_user_params = settings.model_dump(exclude_none=True)
|
||||
|
||||
# Load existing config
|
||||
config = await proxy_config.get_config()
|
||||
|
||||
# Update config with new settings
|
||||
if "litellm_settings" not in config:
|
||||
config["litellm_settings"] = {}
|
||||
|
||||
config["litellm_settings"]["default_internal_user_params"] = settings.model_dump(
|
||||
exclude_none=True
|
||||
return await _update_litellm_setting(
|
||||
settings=settings,
|
||||
settings_key="default_internal_user_params",
|
||||
in_memory_var=litellm.default_internal_user_params,
|
||||
success_message="Internal user settings updated successfully",
|
||||
)
|
||||
|
||||
# Save the updated config
|
||||
await proxy_config.save_config(new_config=config)
|
||||
|
||||
return {
|
||||
"message": "Internal user settings updated successfully",
|
||||
"status": "success",
|
||||
"settings": litellm.default_internal_user_params,
|
||||
}
|
||||
@router.patch(
|
||||
"/update/default_team_settings",
|
||||
tags=["SSO Settings"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
)
|
||||
async def update_default_team_settings(settings: DefaultTeamSSOParams):
|
||||
"""
|
||||
Update the default team parameters for SSO users.
|
||||
These settings will be applied to new teams created from SSO.
|
||||
"""
|
||||
return await _update_litellm_setting(
|
||||
settings=settings,
|
||||
settings_key="default_team_params",
|
||||
in_memory_var=litellm.default_team_params,
|
||||
success_message="Default team settings updated successfully",
|
||||
)
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
from typing import List, Optional, TypedDict
|
||||
from typing import List, Literal, Optional, TypedDict
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from litellm.proxy._types import LiteLLMPydanticObjectBase, LitellmUserRoles
|
||||
|
||||
|
||||
class MicrosoftGraphAPIUserGroupDirectoryObject(TypedDict, total=False):
|
||||
|
@ -25,3 +29,30 @@ class MicrosoftServicePrincipalTeam(TypedDict, total=False):
|
|||
|
||||
principalDisplayName: Optional[str]
|
||||
principalId: Optional[str]
|
||||
|
||||
|
||||
class DefaultTeamSSOParams(LiteLLMPydanticObjectBase):
|
||||
"""
|
||||
Default parameters to apply when a new team is automatically created by LiteLLM via SSO Groups
|
||||
"""
|
||||
|
||||
models: List[str] = Field(
|
||||
default=[],
|
||||
description="Default list of models that new automatically created teams can access",
|
||||
)
|
||||
max_budget: Optional[float] = Field(
|
||||
default=None,
|
||||
description="Default maximum budget (in USD) for new automatically created teams",
|
||||
)
|
||||
budget_duration: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Default budget duration for new automatically created teams (e.g. 'daily', 'weekly', 'monthly')",
|
||||
)
|
||||
tpm_limit: Optional[int] = Field(
|
||||
default=None,
|
||||
description="Default tpm limit for new automatically created teams",
|
||||
)
|
||||
rpm_limit: Optional[int] = Field(
|
||||
default=None,
|
||||
description="Default rpm limit for new automatically created teams",
|
||||
)
|
||||
|
|
|
@ -19,6 +19,7 @@ from litellm.proxy._types import NewTeamRequest
|
|||
from litellm.proxy.auth.handle_jwt import JWTHandler
|
||||
from litellm.proxy.management_endpoints.types import CustomOpenID
|
||||
from litellm.proxy.management_endpoints.ui_sso import (
|
||||
DefaultTeamSSOParams,
|
||||
GoogleSSOHandler,
|
||||
MicrosoftSSOHandler,
|
||||
SSOAuthenticationHandler,
|
||||
|
@ -421,8 +422,10 @@ def test_get_group_ids_from_graph_api_response():
|
|||
@pytest.mark.parametrize(
|
||||
"team_params",
|
||||
[
|
||||
# Test case 1: Using NewTeamRequest
|
||||
NewTeamRequest(max_budget=10, budget_duration="1d", models=["special-gpt-5"]),
|
||||
# Test case 1: Using DefaultTeamSSOParams
|
||||
DefaultTeamSSOParams(
|
||||
max_budget=10, budget_duration="1d", models=["special-gpt-5"]
|
||||
),
|
||||
# Test case 2: Using Dict
|
||||
{"max_budget": 10, "budget_duration": "1d", "models": ["special-gpt-5"]},
|
||||
],
|
||||
|
|
|
@ -11,6 +11,7 @@ sys.path.insert(
|
|||
|
||||
from litellm.proxy._types import DefaultInternalUserParams, LitellmUserRoles
|
||||
from litellm.proxy.proxy_server import app
|
||||
from litellm.types.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
@ -25,7 +26,14 @@ def mock_proxy_config(monkeypatch):
|
|||
"max_budget": 100.0,
|
||||
"budget_duration": "30d",
|
||||
"models": ["gpt-3.5-turbo", "gpt-4"],
|
||||
}
|
||||
},
|
||||
"default_team_params": {
|
||||
"models": ["gpt-3.5-turbo"],
|
||||
"max_budget": 50.0,
|
||||
"budget_duration": "14d",
|
||||
"tpm_limit": 100,
|
||||
"rpm_limit": 10,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,3 +146,76 @@ class TestProxySettingEndpoints:
|
|||
|
||||
# Verify save_config was called exactly once
|
||||
assert mock_proxy_config["save_call_count"]() == 1
|
||||
|
||||
def test_get_default_team_settings(self, mock_proxy_config, mock_auth):
|
||||
"""Test getting the default team settings"""
|
||||
response = client.get("/get/default_team_settings")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Check structure of response
|
||||
assert "values" in data
|
||||
assert "schema" in data
|
||||
|
||||
# Check values match our mock config
|
||||
values = data["values"]
|
||||
mock_params = mock_proxy_config["config"]["litellm_settings"][
|
||||
"default_team_params"
|
||||
]
|
||||
assert values["models"] == mock_params["models"]
|
||||
assert values["max_budget"] == mock_params["max_budget"]
|
||||
assert values["budget_duration"] == mock_params["budget_duration"]
|
||||
assert values["tpm_limit"] == mock_params["tpm_limit"]
|
||||
assert values["rpm_limit"] == mock_params["rpm_limit"]
|
||||
|
||||
# Check schema contains descriptions
|
||||
assert "properties" in data["schema"]
|
||||
assert "models" in data["schema"]["properties"]
|
||||
assert "description" in data["schema"]["properties"]["models"]
|
||||
|
||||
def test_update_default_team_settings(
|
||||
self, mock_proxy_config, mock_auth, monkeypatch
|
||||
):
|
||||
"""Test updating the default team settings"""
|
||||
# Mock litellm.default_team_params
|
||||
import litellm
|
||||
|
||||
monkeypatch.setattr(litellm, "default_team_params", {})
|
||||
|
||||
# New settings to update
|
||||
new_settings = {
|
||||
"models": ["gpt-4", "claude-3"],
|
||||
"max_budget": 150.0,
|
||||
"budget_duration": "30d",
|
||||
"tpm_limit": 200,
|
||||
"rpm_limit": 20,
|
||||
}
|
||||
|
||||
response = client.patch("/update/default_team_settings", json=new_settings)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Check response structure
|
||||
assert data["status"] == "success"
|
||||
assert "settings" in data
|
||||
|
||||
# Verify settings were updated
|
||||
settings = data["settings"]
|
||||
assert settings["models"] == new_settings["models"]
|
||||
assert settings["max_budget"] == new_settings["max_budget"]
|
||||
assert settings["budget_duration"] == new_settings["budget_duration"]
|
||||
assert settings["tpm_limit"] == new_settings["tpm_limit"]
|
||||
assert settings["rpm_limit"] == new_settings["rpm_limit"]
|
||||
|
||||
# Verify the config was updated
|
||||
updated_config = mock_proxy_config["config"]["litellm_settings"][
|
||||
"default_team_params"
|
||||
]
|
||||
assert updated_config["models"] == new_settings["models"]
|
||||
assert updated_config["max_budget"] == new_settings["max_budget"]
|
||||
assert updated_config["tpm_limit"] == new_settings["tpm_limit"]
|
||||
|
||||
# Verify save_config was called exactly once
|
||||
assert mock_proxy_config["save_call_count"]() == 1
|
||||
|
|
307
ui/litellm-dashboard/src/components/TeamSSOSettings.tsx
Normal file
307
ui/litellm-dashboard/src/components/TeamSSOSettings.tsx
Normal file
|
@ -0,0 +1,307 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Card, Title, Text, Divider, Button, TextInput } from "@tremor/react";
|
||||
import { Typography, Spin, message, Switch, Select, Form } from "antd";
|
||||
import { getDefaultTeamSettings, updateDefaultTeamSettings, modelAvailableCall } from "./networking";
|
||||
import BudgetDurationDropdown, { getBudgetDurationLabel } from "./common_components/budget_duration_dropdown";
|
||||
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
|
||||
|
||||
interface TeamSSOSettingsProps {
|
||||
accessToken: string | null;
|
||||
userID: string;
|
||||
userRole: string;
|
||||
}
|
||||
|
||||
const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken, userID, userRole }) => {
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [settings, setSettings] = useState<any>(null);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [editedValues, setEditedValues] = useState<any>({});
|
||||
const [saving, setSaving] = useState<boolean>(false);
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
const { Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTeamSSOSettings = async () => {
|
||||
if (!accessToken) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await getDefaultTeamSettings(accessToken);
|
||||
setSettings(data);
|
||||
setEditedValues(data.values || {});
|
||||
|
||||
// Fetch available models
|
||||
if (accessToken) {
|
||||
try {
|
||||
const modelResponse = await modelAvailableCall(accessToken, userID, userRole);
|
||||
if (modelResponse && modelResponse.data) {
|
||||
const modelNames = modelResponse.data.map((model: { id: string }) => model.id);
|
||||
setAvailableModels(modelNames);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching available models:", error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching team SSO settings:", error);
|
||||
message.error("Failed to fetch team settings");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTeamSSOSettings();
|
||||
}, [accessToken]);
|
||||
|
||||
const handleSaveSettings = async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const updatedSettings = await updateDefaultTeamSettings(accessToken, editedValues);
|
||||
setSettings({...settings, values: updatedSettings.settings});
|
||||
setIsEditing(false);
|
||||
message.success("Default team settings updated successfully");
|
||||
} catch (error) {
|
||||
console.error("Error updating team settings:", error);
|
||||
message.error("Failed to update team settings");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTextInputChange = (key: string, value: any) => {
|
||||
setEditedValues((prev: Record<string, any>) => ({
|
||||
...prev,
|
||||
[key]: value
|
||||
}));
|
||||
};
|
||||
|
||||
const renderEditableField = (key: string, property: any, value: any) => {
|
||||
const type = property.type;
|
||||
|
||||
if (key === "budget_duration") {
|
||||
return (
|
||||
<BudgetDurationDropdown
|
||||
value={editedValues[key] || null}
|
||||
onChange={(value) => handleTextInputChange(key, value)}
|
||||
className="mt-2"
|
||||
/>
|
||||
);
|
||||
} else if (type === "boolean") {
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<Switch
|
||||
checked={!!editedValues[key]}
|
||||
onChange={(checked) => handleTextInputChange(key, checked)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (type === "array" && property.items?.enum) {
|
||||
return (
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{ width: '100%' }}
|
||||
value={editedValues[key] || []}
|
||||
onChange={(value) => handleTextInputChange(key, value)}
|
||||
className="mt-2"
|
||||
>
|
||||
{property.items.enum.map((option: string) => (
|
||||
<Option key={option} value={option}>{option}</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
} else if (key === "models") {
|
||||
return (
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{ width: '100%' }}
|
||||
value={editedValues[key] || []}
|
||||
onChange={(value) => handleTextInputChange(key, value)}
|
||||
className="mt-2"
|
||||
>
|
||||
{availableModels.map((model: string) => (
|
||||
<Option key={model} value={model}>
|
||||
{getModelDisplayName(model)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
} else if (type === "string" && property.enum) {
|
||||
return (
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
value={editedValues[key] || ""}
|
||||
onChange={(value) => handleTextInputChange(key, value)}
|
||||
className="mt-2"
|
||||
>
|
||||
{property.enum.map((option: string) => (
|
||||
<Option key={option} value={option}>{option}</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<TextInput
|
||||
value={editedValues[key] !== undefined ? String(editedValues[key]) : ""}
|
||||
onChange={(e) => handleTextInputChange(key, e.target.value)}
|
||||
placeholder={property.description || ""}
|
||||
className="mt-2"
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderValue = (key: string, value: any): JSX.Element => {
|
||||
if (value === null || value === undefined) return <span className="text-gray-400">Not set</span>;
|
||||
|
||||
if (key === "budget_duration") {
|
||||
return <span>{getBudgetDurationLabel(value)}</span>;
|
||||
}
|
||||
|
||||
if (typeof value === "boolean") {
|
||||
return <span>{value ? "Enabled" : "Disabled"}</span>;
|
||||
}
|
||||
|
||||
if (key === "models" && Array.isArray(value)) {
|
||||
if (value.length === 0) return <span className="text-gray-400">None</span>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{value.map((model, index) => (
|
||||
<span key={index} className="px-2 py-1 bg-blue-100 rounded text-xs">
|
||||
{getModelDisplayName(model)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === "object") {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) return <span className="text-gray-400">None</span>;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mt-1">
|
||||
{value.map((item, index) => (
|
||||
<span key={index} className="px-2 py-1 bg-blue-100 rounded text-xs">
|
||||
{typeof item === "object" ? JSON.stringify(item) : String(item)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">
|
||||
{JSON.stringify(value, null, 2)}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
return <span>{String(value)}</span>;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!settings) {
|
||||
return (
|
||||
<Card>
|
||||
<Text>No team settings available or you do not have permission to view them.</Text>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Dynamically render settings based on the schema
|
||||
const renderSettings = () => {
|
||||
const { values, schema } = settings;
|
||||
|
||||
if (!schema || !schema.properties) {
|
||||
return <Text>No schema information available</Text>;
|
||||
}
|
||||
|
||||
return Object.entries(schema.properties).map(([key, property]: [string, any]) => {
|
||||
const value = values[key];
|
||||
const displayName = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||
|
||||
return (
|
||||
<div key={key} className="mb-6 pb-6 border-b border-gray-200 last:border-0">
|
||||
<Text className="font-medium text-lg">{displayName}</Text>
|
||||
<Paragraph className="text-sm text-gray-500 mt-1">
|
||||
{property.description || "No description available"}
|
||||
</Paragraph>
|
||||
|
||||
{isEditing ? (
|
||||
<div className="mt-2">
|
||||
{renderEditableField(key, property, value)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-1 p-2 bg-gray-50 rounded">
|
||||
{renderValue(key, value)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title className="text-xl">Default Team Settings</Title>
|
||||
{!loading && settings && (
|
||||
isEditing ? (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
setEditedValues(settings.values || {});
|
||||
}}
|
||||
disabled={saving}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSaveSettings}
|
||||
loading={saving}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
Edit Settings
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Text>
|
||||
These settings will be applied by default when creating new teams.
|
||||
</Text>
|
||||
|
||||
{settings?.schema?.description && (
|
||||
<Paragraph className="mb-4 mt-2">{settings.schema.description}</Paragraph>
|
||||
)}
|
||||
<Divider />
|
||||
|
||||
<div className="mt-4 space-y-4">
|
||||
{renderSettings()}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default TeamSSOSettings;
|
|
@ -4344,4 +4344,71 @@ export const tagDeleteCall = async (
|
|||
console.error("Error deleting tag:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getDefaultTeamSettings = async (accessToken: string) => {
|
||||
try {
|
||||
// Construct base URL
|
||||
let url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/get/default_team_settings`
|
||||
: `/get/default_team_settings`;
|
||||
|
||||
console.log("Fetching default team settings from:", url);
|
||||
|
||||
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("Fetched default team settings:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch default team settings:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const updateDefaultTeamSettings = async (accessToken: string, settings: Record<string, any>) => {
|
||||
try {
|
||||
// Construct base URL
|
||||
let url = proxyBaseUrl
|
||||
? `${proxyBaseUrl}/update/default_team_settings`
|
||||
: `/update/default_team_settings`;
|
||||
|
||||
console.log("Updating default team settings:", settings);
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
|
||||
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("Updated default team settings:", data);
|
||||
message.success("Default team settings updated successfully");
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to update default team settings:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
|
@ -27,6 +27,8 @@ import { Select, SelectItem } from "@tremor/react";
|
|||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { getGuardrailsList } from "./networking";
|
||||
import TeamInfoView from "@/components/team/team_info";
|
||||
import TeamSSOSettings from "@/components/TeamSSOSettings";
|
||||
import { isAdminRole } from "@/utils/roles";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
|
@ -354,6 +356,7 @@ const Teams: React.FC<TeamProps> = ({
|
|||
<div className="flex">
|
||||
<Tab>Your Teams</Tab>
|
||||
<Tab>Available Teams</Tab>
|
||||
{isAdminRole(userRole || "") && <Tab>Default Team Settings</Tab>}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
{lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>}
|
||||
|
@ -797,6 +800,15 @@ const Teams: React.FC<TeamProps> = ({
|
|||
userID={userID}
|
||||
/>
|
||||
</TabPanel>
|
||||
{isAdminRole(userRole || "") && (
|
||||
<TabPanel>
|
||||
<TeamSSOSettings
|
||||
accessToken={accessToken}
|
||||
userID={userID || ""}
|
||||
userRole={userRole || ""}
|
||||
/>
|
||||
</TabPanel>
|
||||
)}
|
||||
</TabPanels>
|
||||
|
||||
</TabGroup>)}
|
||||
|
|
|
@ -5,4 +5,10 @@ export const all_admin_roles = [...old_admin_roles, ...v2_admin_role_names];
|
|||
|
||||
export const internalUserRoles = ["Internal User", "Internal Viewer"];
|
||||
export const rolesAllowedToSeeUsage = ["Admin", "Admin Viewer", "Internal User", "Internal Viewer"];
|
||||
export const rolesWithWriteAccess = ["Internal User", "Admin"];
|
||||
export const rolesWithWriteAccess = ["Internal User", "Admin"];
|
||||
|
||||
// Helper function to check if a role is in all_admin_roles
|
||||
export const isAdminRole = (role: string): boolean => {
|
||||
return all_admin_roles.includes(role);
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue