mirror of
https://github.com/BerriAI/litellm.git
synced 2025-04-27 11:43:54 +00:00
fix display of settings
This commit is contained in:
parent
f69f330400
commit
0c0c7bf2f5
4 changed files with 166 additions and 76 deletions
|
@ -2653,32 +2653,6 @@ class TeamKeyGenerationSettings(LiteLLMPydanticObjectBase):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PersonalKeyGenerationSettings(LiteLLMPydanticObjectBase):
|
|
||||||
"""
|
|
||||||
Settings for personal key generation (maps to 'Default Team' on UI)
|
|
||||||
"""
|
|
||||||
|
|
||||||
allowed_user_roles: Optional[List[str]] = Field(
|
|
||||||
default=[LitellmUserRoles.PROXY_ADMIN],
|
|
||||||
description="User roles that are allowed to generate personal keys (for the 'Default Team')",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class KeyGenerationSettings(LiteLLMPydanticObjectBase):
|
|
||||||
"""
|
|
||||||
Settings that restrict who can generate keys
|
|
||||||
"""
|
|
||||||
|
|
||||||
team_key_generation: Optional[TeamKeyGenerationSettings] = Field(
|
|
||||||
default=None,
|
|
||||||
description="Settings that control which team members can generate keys for their team",
|
|
||||||
)
|
|
||||||
personal_key_generation: Optional[PersonalKeyGenerationSettings] = Field(
|
|
||||||
default=None,
|
|
||||||
description="Settings that control which users can generate personal keys",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultInternalUserParams(LiteLLMPydanticObjectBase):
|
class DefaultInternalUserParams(LiteLLMPydanticObjectBase):
|
||||||
"""
|
"""
|
||||||
Default parameters to apply when a new user signs in via SSO
|
Default parameters to apply when a new user signs in via SSO
|
||||||
|
@ -2706,14 +2680,3 @@ class DefaultInternalUserParams(LiteLLMPydanticObjectBase):
|
||||||
models: Optional[List[str]] = Field(
|
models: Optional[List[str]] = Field(
|
||||||
default=None, description="Default list of models that new users can access"
|
default=None, description="Default list of models that new users can access"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class UISSOSettings(LiteLLMPydanticObjectBase):
|
|
||||||
"""
|
|
||||||
Configuration for SSO integration with the LiteLLM proxy UI
|
|
||||||
"""
|
|
||||||
|
|
||||||
default_internal_user_params: Optional[DefaultInternalUserParams] = Field(
|
|
||||||
default=None,
|
|
||||||
description="Default parameters applied to new users signing in via SSO",
|
|
||||||
)
|
|
||||||
|
|
|
@ -112,7 +112,7 @@ async def delete_allowed_ip(ip_address: IPAddress):
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/sso_settings",
|
"/get/internal_user_settings",
|
||||||
tags=["SSO Settings"],
|
tags=["SSO Settings"],
|
||||||
dependencies=[Depends(user_api_key_auth)],
|
dependencies=[Depends(user_api_key_auth)],
|
||||||
)
|
)
|
||||||
|
@ -124,27 +124,15 @@ async def get_sso_settings():
|
||||||
from pydantic import TypeAdapter
|
from pydantic import TypeAdapter
|
||||||
|
|
||||||
# Create the settings object first
|
# Create the settings object first
|
||||||
sso_settings = UISSOSettings(
|
sso_settings = DefaultInternalUserParams(
|
||||||
max_internal_user_budget=litellm.max_internal_user_budget,
|
**(
|
||||||
internal_user_budget_duration=litellm.internal_user_budget_duration,
|
litellm.default_internal_user_params
|
||||||
default_internal_user_params=DefaultInternalUserParams(
|
if isinstance(litellm.default_internal_user_params, dict)
|
||||||
**(
|
else {}
|
||||||
litellm.default_internal_user_params
|
)
|
||||||
if isinstance(litellm.default_internal_user_params, dict)
|
|
||||||
else {}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
upperbound_key_generate_params=UpperboundKeyGenerateParams(
|
|
||||||
**(
|
|
||||||
litellm.upperbound_key_generate_params
|
|
||||||
if isinstance(litellm.upperbound_key_generate_params, dict)
|
|
||||||
else {}
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get the schema for UISSOSettings
|
# Get the schema for UISSOSettings
|
||||||
schema = TypeAdapter(UISSOSettings).json_schema(by_alias=True)
|
schema = TypeAdapter(DefaultInternalUserParams).json_schema(by_alias=True)
|
||||||
|
|
||||||
# Convert to dict for response
|
# Convert to dict for response
|
||||||
settings_dict = sso_settings.model_dump()
|
settings_dict = sso_settings.model_dump()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Card, Title, Text, Divider } from "@tremor/react";
|
import { Card, Title, Text, Divider, Button, TextInput } from "@tremor/react";
|
||||||
import { Typography, Spin, message } from "antd";
|
import { Typography, Spin, message, Switch, Select, Form } from "antd";
|
||||||
import { getSSOSettingsCall } from "./networking";
|
import { getInternalUserSettings, updateInternalUserSettings } from "./networking";
|
||||||
|
|
||||||
interface SSOSettingsProps {
|
interface SSOSettingsProps {
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
|
@ -10,7 +10,11 @@ interface SSOSettingsProps {
|
||||||
const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
|
const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [settings, setSettings] = useState<any>(null);
|
const [settings, setSettings] = useState<any>(null);
|
||||||
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
|
const [editedValues, setEditedValues] = useState<any>({});
|
||||||
|
const [saving, setSaving] = useState<boolean>(false);
|
||||||
const { Paragraph } = Typography;
|
const { Paragraph } = Typography;
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSSOSettings = async () => {
|
const fetchSSOSettings = async () => {
|
||||||
|
@ -20,8 +24,9 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await getSSOSettingsCall(accessToken);
|
const data = await getInternalUserSettings(accessToken);
|
||||||
setSettings(data);
|
setSettings(data);
|
||||||
|
setEditedValues(data.values || {});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching SSO settings:", error);
|
console.error("Error fetching SSO settings:", error);
|
||||||
message.error("Failed to fetch SSO settings");
|
message.error("Failed to fetch SSO settings");
|
||||||
|
@ -33,12 +38,105 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
|
||||||
fetchSSOSettings();
|
fetchSSOSettings();
|
||||||
}, [accessToken]);
|
}, [accessToken]);
|
||||||
|
|
||||||
|
const handleSaveSettings = async () => {
|
||||||
|
if (!accessToken) return;
|
||||||
|
|
||||||
|
setSaving(true);
|
||||||
|
try {
|
||||||
|
await updateInternalUserSettings(accessToken, editedValues);
|
||||||
|
setSettings({...settings, values: editedValues});
|
||||||
|
setIsEditing(false);
|
||||||
|
message.success("Settings updated successfully");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating SSO settings:", error);
|
||||||
|
message.error("Failed to update settings");
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTextInputChange = (key: string, value: any) => {
|
||||||
|
setEditedValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
[key]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderEditableField = (key: string, property: any, value: any) => {
|
||||||
|
const type = property.type;
|
||||||
|
|
||||||
|
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 (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 = (value: any): JSX.Element => {
|
const renderValue = (value: any): JSX.Element => {
|
||||||
if (value === null) return <span className="text-gray-400">Not set</span>;
|
if (value === null || value === undefined) return <span className="text-gray-400">Not set</span>;
|
||||||
|
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
return <span>{value ? "Enabled" : "Disabled"}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof value === "object") {
|
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 (
|
return (
|
||||||
<pre className="bg-gray-50 p-2 rounded overflow-auto max-h-60">
|
<pre className="bg-gray-100 p-2 rounded text-xs overflow-auto mt-1">
|
||||||
{JSON.stringify(value, null, 2)}
|
{JSON.stringify(value, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
||||||
|
@ -58,7 +156,7 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
|
||||||
if (!settings) {
|
if (!settings) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<Title>Personal Key Creation</Title>
|
<Title>SSO Settings</Title>
|
||||||
<Text>No settings available or you don't have permission to view them.</Text>
|
<Text>No settings available or you don't have permission to view them.</Text>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
@ -74,16 +172,24 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
|
||||||
|
|
||||||
return Object.entries(schema.properties).map(([key, property]: [string, any]) => {
|
return Object.entries(schema.properties).map(([key, property]: [string, any]) => {
|
||||||
const value = values[key];
|
const value = values[key];
|
||||||
|
const displayName = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={key} className="mb-6">
|
<div key={key} className="mb-6 pb-6 border-b border-gray-200 last:border-0">
|
||||||
<Text className="font-medium text-lg">{key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}</Text>
|
<Text className="font-medium text-lg">{displayName}</Text>
|
||||||
<Paragraph className="text-sm text-gray-500 mt-1">
|
<Paragraph className="text-sm text-gray-500 mt-1">
|
||||||
{property.description || "No description available"}
|
{property.description || "No description available"}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
<div className="mt-2 p-3 bg-gray-50 rounded-md">
|
|
||||||
{renderValue(value)}
|
{isEditing ? (
|
||||||
</div>
|
<div className="mt-2">
|
||||||
|
{renderEditableField(key, property, value)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="mt-1 p-2 bg-gray-50 rounded">
|
||||||
|
{renderValue(value)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -91,13 +197,46 @@ const SSOSettings: React.FC<SSOSettingsProps> = ({ accessToken }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<Title>SSO Settings</Title>
|
<div className="flex justify-between items-center mb-4">
|
||||||
{settings.schema?.description && (
|
<Title>SSO Settings</Title>
|
||||||
<Paragraph>{settings.schema.description}</Paragraph>
|
{!loading && settings && (
|
||||||
|
isEditing ? (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
color="gray"
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditing(false);
|
||||||
|
setEditedValues(settings.values || {});
|
||||||
|
}}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="blue"
|
||||||
|
onClick={handleSaveSettings}
|
||||||
|
loading={saving}
|
||||||
|
>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
>
|
||||||
|
Edit Settings
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{settings?.schema?.description && (
|
||||||
|
<Paragraph className="mb-4">{settings.schema.description}</Paragraph>
|
||||||
)}
|
)}
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4 space-y-4">
|
||||||
{renderSettings()}
|
{renderSettings()}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
@ -3974,12 +3974,12 @@ export const uiSpendLogDetailsCall = async (
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getSSOSettingsCall = async (accessToken: string) => {
|
export const getInternalUserSettings = async (accessToken: string) => {
|
||||||
try {
|
try {
|
||||||
// Construct base URL
|
// Construct base URL
|
||||||
let url = proxyBaseUrl
|
let url = proxyBaseUrl
|
||||||
? `${proxyBaseUrl}/sso_settings`
|
? `${proxyBaseUrl}/get/internal_user_settings`
|
||||||
: `/sso_settings`;
|
: `/get/internal_user_settings`;
|
||||||
|
|
||||||
console.log("Fetching SSO settings from:", url);
|
console.log("Fetching SSO settings from:", url);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue