[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:
Ishaan Jaff 2025-04-11 14:07:10 -07:00 committed by GitHub
parent 92b0799690
commit b12c7d04c8
10 changed files with 646 additions and 61 deletions

View file

@ -65,8 +65,8 @@ from litellm.proxy._types import (
KeyManagementSystem, KeyManagementSystem,
KeyManagementSettings, KeyManagementSettings,
LiteLLM_UpperboundKeyGenerateParams, LiteLLM_UpperboundKeyGenerateParams,
NewTeamRequest,
) )
from litellm.types.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams
from litellm.types.utils import StandardKeyGenerationConfig, LlmProviders from litellm.types.utils import StandardKeyGenerationConfig, LlmProviders
from litellm.integrations.custom_logger import CustomLogger from litellm.integrations.custom_logger import CustomLogger
from litellm.litellm_core_utils.logging_callback_manager import LoggingCallbackManager 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 upperbound_key_generate_params: Optional[LiteLLM_UpperboundKeyGenerateParams] = None
key_generation_settings: Optional[StandardKeyGenerationConfig] = None key_generation_settings: Optional[StandardKeyGenerationConfig] = None
default_internal_user_params: Optional[Dict] = 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 default_team_settings: Optional[List] = None
max_user_budget: Optional[float] = None max_user_budget: Optional[float] = None
default_max_internal_user_budget: Optional[float] = None default_max_internal_user_budget: Optional[float] = None

View file

@ -941,7 +941,7 @@ class SSOAuthenticationHandler:
@staticmethod @staticmethod
def _cast_and_deepcopy_litellm_default_team_params( def _cast_and_deepcopy_litellm_default_team_params(
default_team_params: Union[NewTeamRequest, Dict], default_team_params: Union[DefaultTeamSSOParams, Dict],
team_request: NewTeamRequest, team_request: NewTeamRequest,
litellm_team_id: str, litellm_team_id: str,
litellm_team_name: Optional[str] = None, litellm_team_name: Optional[str] = None,
@ -949,23 +949,20 @@ class SSOAuthenticationHandler:
""" """
Casts and deepcopies the litellm.default_team_params to a NewTeamRequest object Casts and deepcopies the litellm.default_team_params to a NewTeamRequest object
- Ensures we create a new NewTeamRequest object - Ensures we create a new DefaultTeamSSOParams object
- Handle the case where litellm.default_team_params is a dict or a NewTeamRequest 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 NewTeamRequest object - Adds the litellm_team_id and litellm_team_name to the DefaultTeamSSOParams object
""" """
if isinstance(default_team_params, dict): if isinstance(default_team_params, dict):
_team_request = deepcopy(default_team_params) _team_request = deepcopy(default_team_params)
_team_request["team_id"] = litellm_team_id _team_request["team_id"] = litellm_team_id
_team_request["team_alias"] = litellm_team_name _team_request["team_alias"] = litellm_team_name
team_request = NewTeamRequest(**_team_request) team_request = NewTeamRequest(**_team_request)
elif isinstance(litellm.default_team_params, NewTeamRequest): elif isinstance(litellm.default_team_params, DefaultTeamSSOParams):
team_request = litellm.default_team_params.model_copy( _default_team_params = deepcopy(litellm.default_team_params)
deep=True, _new_team_request = team_request.model_dump()
update={ _new_team_request.update(_default_team_params)
"team_id": litellm_team_id, team_request = NewTeamRequest(**_new_team_request)
"team_alias": litellm_team_name,
},
)
return team_request return team_request

View file

@ -1,5 +1,5 @@
#### CRUD ENDPOINTS for UI Settings ##### #### CRUD ENDPOINTS for UI Settings #####
from typing import List from typing import Any, List, Union
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
@ -7,6 +7,7 @@ import litellm
from litellm._logging import verbose_proxy_logger from litellm._logging import verbose_proxy_logger
from litellm.proxy._types import * from litellm.proxy._types import *
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.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams
router = APIRouter() 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"} return {"message": f"IP {ip_address.ip} deleted successfully", "status": "success"}
@router.get( async def _get_settings_with_schema(
"/get/internal_user_settings", settings_key: str,
tags=["SSO Settings"], settings_class: Any,
dependencies=[Depends(user_api_key_auth)], config: dict,
) ) -> dict:
async def get_sso_settings():
""" """
Get all SSO settings from the litellm_settings configuration. Common utility function to get settings with schema information.
Returns a structured object with values and descriptions for UI display.
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 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 {} litellm_settings = config.get("litellm_settings", {}) or {}
default_internal_user_params = ( settings_data = litellm_settings.get(settings_key, {}) or {}
litellm_settings.get("default_internal_user_params", {}) or {}
)
# Create the settings object first # Create the settings object
sso_settings = DefaultInternalUserParams(**(default_internal_user_params)) settings = settings_class(**(settings_data))
# Get the schema for UISSOSettings # Get the schema
schema = TypeAdapter(DefaultInternalUserParams).json_schema(by_alias=True) schema = TypeAdapter(settings_class).json_schema(by_alias=True)
# Convert to dict for response # Convert to dict for response
settings_dict = sso_settings.model_dump() settings_dict = settings.model_dump()
# Add descriptions to the response # Add descriptions to the response
result = { result = {
@ -166,6 +164,89 @@ async def get_sso_settings():
return result 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( @router.patch(
"/update/internal_user_settings", "/update/internal_user_settings",
tags=["SSO 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. Update the default internal user parameters for SSO users.
These settings will be applied to new users who sign in via SSO. These settings will be applied to new users who sign in via SSO.
""" """
from litellm.proxy.proxy_server import proxy_config return await _update_litellm_setting(
settings=settings,
# Update the in-memory settings settings_key="default_internal_user_params",
litellm.default_internal_user_params = settings.model_dump(exclude_none=True) in_memory_var=litellm.default_internal_user_params,
success_message="Internal user settings updated successfully",
# 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
) )
# Save the updated config
await proxy_config.save_config(new_config=config)
return { @router.patch(
"message": "Internal user settings updated successfully", "/update/default_team_settings",
"status": "success", tags=["SSO Settings"],
"settings": litellm.default_internal_user_params, 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",
)

View file

@ -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): class MicrosoftGraphAPIUserGroupDirectoryObject(TypedDict, total=False):
@ -25,3 +29,30 @@ class MicrosoftServicePrincipalTeam(TypedDict, total=False):
principalDisplayName: Optional[str] principalDisplayName: Optional[str]
principalId: 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",
)

View file

@ -19,6 +19,7 @@ from litellm.proxy._types import NewTeamRequest
from litellm.proxy.auth.handle_jwt import JWTHandler from litellm.proxy.auth.handle_jwt import JWTHandler
from litellm.proxy.management_endpoints.types import CustomOpenID from litellm.proxy.management_endpoints.types import CustomOpenID
from litellm.proxy.management_endpoints.ui_sso import ( from litellm.proxy.management_endpoints.ui_sso import (
DefaultTeamSSOParams,
GoogleSSOHandler, GoogleSSOHandler,
MicrosoftSSOHandler, MicrosoftSSOHandler,
SSOAuthenticationHandler, SSOAuthenticationHandler,
@ -421,8 +422,10 @@ def test_get_group_ids_from_graph_api_response():
@pytest.mark.parametrize( @pytest.mark.parametrize(
"team_params", "team_params",
[ [
# Test case 1: Using NewTeamRequest # Test case 1: Using DefaultTeamSSOParams
NewTeamRequest(max_budget=10, budget_duration="1d", models=["special-gpt-5"]), DefaultTeamSSOParams(
max_budget=10, budget_duration="1d", models=["special-gpt-5"]
),
# Test case 2: Using Dict # Test case 2: Using Dict
{"max_budget": 10, "budget_duration": "1d", "models": ["special-gpt-5"]}, {"max_budget": 10, "budget_duration": "1d", "models": ["special-gpt-5"]},
], ],

View file

@ -11,6 +11,7 @@ sys.path.insert(
from litellm.proxy._types import DefaultInternalUserParams, LitellmUserRoles from litellm.proxy._types import DefaultInternalUserParams, LitellmUserRoles
from litellm.proxy.proxy_server import app from litellm.proxy.proxy_server import app
from litellm.types.proxy.management_endpoints.ui_sso import DefaultTeamSSOParams
client = TestClient(app) client = TestClient(app)
@ -25,7 +26,14 @@ def mock_proxy_config(monkeypatch):
"max_budget": 100.0, "max_budget": 100.0,
"budget_duration": "30d", "budget_duration": "30d",
"models": ["gpt-3.5-turbo", "gpt-4"], "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 # Verify save_config was called exactly once
assert mock_proxy_config["save_call_count"]() == 1 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

View 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;

View file

@ -4344,4 +4344,71 @@ export const tagDeleteCall = async (
console.error("Error deleting tag:", error); console.error("Error deleting tag:", error);
throw 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;
}
}; };

View file

@ -27,6 +27,8 @@ import { Select, SelectItem } from "@tremor/react";
import { InfoCircleOutlined } from '@ant-design/icons'; import { InfoCircleOutlined } from '@ant-design/icons';
import { getGuardrailsList } from "./networking"; import { getGuardrailsList } from "./networking";
import TeamInfoView from "@/components/team/team_info"; import TeamInfoView from "@/components/team/team_info";
import TeamSSOSettings from "@/components/TeamSSOSettings";
import { isAdminRole } from "@/utils/roles";
import { import {
Table, Table,
TableBody, TableBody,
@ -354,6 +356,7 @@ const Teams: React.FC<TeamProps> = ({
<div className="flex"> <div className="flex">
<Tab>Your Teams</Tab> <Tab>Your Teams</Tab>
<Tab>Available Teams</Tab> <Tab>Available Teams</Tab>
{isAdminRole(userRole || "") && <Tab>Default Team Settings</Tab>}
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>} {lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>}
@ -797,6 +800,15 @@ const Teams: React.FC<TeamProps> = ({
userID={userID} userID={userID}
/> />
</TabPanel> </TabPanel>
{isAdminRole(userRole || "") && (
<TabPanel>
<TeamSSOSettings
accessToken={accessToken}
userID={userID || ""}
userRole={userRole || ""}
/>
</TabPanel>
)}
</TabPanels> </TabPanels>
</TabGroup>)} </TabGroup>)}

View file

@ -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 internalUserRoles = ["Internal User", "Internal Viewer"];
export const rolesAllowedToSeeUsage = ["Admin", "Admin Viewer", "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);
};