feat(ui): i18n for SCIM, SSO modals, team SSO settings, and tool detail

This commit is contained in:
吹雪 2026-06-10 12:43:20 +09:00
parent 7bf4de9997
commit 2980302d96
6 changed files with 422 additions and 147 deletions

View file

@ -12,6 +12,7 @@ import {
} from "@ant-design/icons";
import { parseErrorMessage } from "./shared/errorUtils";
import NotificationsManager from "./molecules/notifications_manager";
import { useTranslation } from "react-i18next";
interface SCIMConfigProps {
accessToken: string | null;
@ -20,6 +21,7 @@ interface SCIMConfigProps {
}
const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySettings }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const [isCreatingToken, setIsCreatingToken] = useState(false);
const [tokenData, setTokenData] = useState<any>(null);
@ -42,7 +44,7 @@ const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySetti
const handleCreateSCIMToken = async (values: any) => {
if (!accessToken || !userID) {
NotificationsManager.fromBackend("You need to be logged in to create a SCIM token");
NotificationsManager.fromBackend(t("scim.needToBeLoggedIn"));
return;
}
@ -58,10 +60,10 @@ const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySetti
const response = await keyCreateCall(accessToken, userID, formData);
setTokenData(response);
NotificationsManager.success("SCIM token created successfully");
NotificationsManager.success(t("scim.createTokenSuccess"));
} catch (error: any) {
console.error("Error creating SCIM token:", error);
NotificationsManager.fromBackend("Failed to create SCIM token: " + parseErrorMessage(error));
NotificationsManager.fromBackend(t("scim.createTokenFailed", { error: parseErrorMessage(error) }));
} finally {
setIsCreatingToken(false);
}
@ -71,12 +73,9 @@ const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySetti
<Grid numItems={1}>
<Card>
<div className="flex items-center mb-4">
<Title>SCIM Configuration</Title>
<Title>{t("scim.title")}</Title>
</div>
<Text className="text-gray-600">
System for Cross-domain Identity Management (SCIM) allows you to automatically provision and manage users and
groups in LiteLLM.
</Text>
<Text className="text-gray-600">{t("scim.description")}</Text>
<Divider />
@ -89,21 +88,16 @@ const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySetti
</div>
<Title className="text-lg flex items-center">
<LinkOutlined className="h-5 w-5 mr-2" />
SCIM Tenant URL
{t("scim.tenantUrlTitle")}
</Title>
</div>
<Text className="text-gray-600 mb-3">
Use this URL in your identity provider SCIM integration settings.
</Text>
<Text className="text-gray-600 mb-3">{t("scim.tenantUrlDesc")}</Text>
<div className="flex items-center">
<TextInput value={scimBaseUrl} disabled={true} className="flex-grow" />
<CopyToClipboard
text={scimBaseUrl}
onCopy={() => NotificationsManager.success("URL copied to clipboard")}
>
<CopyToClipboard text={scimBaseUrl} onCopy={() => NotificationsManager.success(t("scim.urlCopied"))}>
<TremorButton variant="primary" className="ml-2 flex items-center">
<CopyOutlined className="h-4 w-4 mr-1" />
Copy
{t("common.copy")}
</TremorButton>
</CopyToClipboard>
</div>
@ -117,13 +111,12 @@ const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySetti
</div>
<Title className="text-lg flex items-center">
<KeyOutlined className="h-5 w-5 mr-2" />
Authentication Token
{t("scim.authTokenTitle")}
</Title>
</div>
<Callout title="Using SCIM" color="blue" className="mb-4">
You need a SCIM token to authenticate with the SCIM API. Create one below and use it in your SCIM provider
configuration.
<Callout title={t("scim.usingScimCalloutTitle")} color="blue" className="mb-4">
{t("scim.calloutDesc")}
</Callout>
{!tokenData ? (
@ -131,10 +124,10 @@ const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySetti
<Form form={form} onFinish={handleCreateSCIMToken} layout="vertical">
<Form.Item
name="key_alias"
label="Token Name"
rules={[{ required: true, message: "Please enter a name for your token" }]}
label={t("scim.tokenNameLabel")}
rules={[{ required: true, message: t("scim.tokenNameRequired") }]}
>
<TextInput placeholder="SCIM Access Token" />
<TextInput placeholder={t("scim.tokenNamePlaceholder")} />
</Form.Item>
<Form.Item>
<TremorButton
@ -144,7 +137,7 @@ const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySetti
className="flex items-center"
>
<KeyOutlined className="h-4 w-4 mr-1" />
Create SCIM Token
{t("scim.createTokenButton")}
</TremorButton>
</Form.Item>
</Form>
@ -153,11 +146,9 @@ const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySetti
<Card className="border border-yellow-300 bg-yellow-50">
<div className="flex items-center mb-2 text-yellow-800">
<ExclamationCircleOutlined className="h-5 w-5 mr-2" />
<Title className="text-lg text-yellow-800">Your SCIM Token</Title>
<Title className="text-lg text-yellow-800">{t("scim.yourScimTokenTitle")}</Title>
</div>
<Text className="text-yellow-800 mb-4 font-medium">
Make sure to copy this token now. You will not be able to see it again.
</Text>
<Text className="text-yellow-800 mb-4 font-medium">{t("scim.tokenWarning")}</Text>
<div className="flex items-center">
<TextInput
value={tokenData.key}
@ -167,17 +158,17 @@ const SCIMConfig: React.FC<SCIMConfigProps> = ({ accessToken, userID, proxySetti
/>
<CopyToClipboard
text={tokenData.key}
onCopy={() => NotificationsManager.success("Token copied to clipboard")}
onCopy={() => NotificationsManager.success(t("scim.tokenCopied"))}
>
<TremorButton variant="primary" className="flex items-center">
<CopyOutlined className="h-4 w-4 mr-1" />
Copy
{t("common.copy")}
</TremorButton>
</CopyToClipboard>
</div>
<TremorButton className="mt-4 flex items-center" variant="secondary" onClick={() => setTokenData(null)}>
<PlusCircleOutlined className="h-4 w-4 mr-1" />
Create Another Token
{t("scim.createAnotherToken")}
</TremorButton>
</Card>
)}

View file

@ -4,6 +4,7 @@ import { Text, TextInput } from "@tremor/react";
import { getSSOSettings, updateSSOSettings } from "./networking";
import NotificationsManager from "./molecules/notifications_manager";
import { parseErrorMessage } from "./shared/errorUtils";
import { useTranslation } from "react-i18next";
interface SSOModalsProps {
isAddSSOModalVisible: boolean;
@ -101,6 +102,19 @@ const ssoProviderConfigs: Record<string, SSOProviderConfig> = {
},
};
const fieldLabelKeys: Record<string, string> = {
google_client_id: "ssoModals.fieldLabels.googleClientId",
google_client_secret: "ssoModals.fieldLabels.googleClientSecret",
microsoft_client_id: "ssoModals.fieldLabels.microsoftClientId",
microsoft_client_secret: "ssoModals.fieldLabels.microsoftClientSecret",
microsoft_tenant: "ssoModals.fieldLabels.microsoftTenant",
generic_client_id: "ssoModals.fieldLabels.genericClientId",
generic_client_secret: "ssoModals.fieldLabels.genericClientSecret",
generic_authorization_endpoint: "ssoModals.fieldLabels.authorizationEndpoint",
generic_token_endpoint: "ssoModals.fieldLabels.tokenEndpoint",
generic_userinfo_endpoint: "ssoModals.fieldLabels.userinfoEndpoint",
};
const SSOModals: React.FC<SSOModalsProps> = ({
isAddSSOModalVisible,
isInstructionsModalVisible,
@ -113,6 +127,7 @@ const SSOModals: React.FC<SSOModalsProps> = ({
accessToken,
ssoConfigured = false, // Default to false if not provided
}) => {
const { t } = useTranslation();
const [isClearConfirmModalVisible, setIsClearConfirmModalVisible] = useState(false);
// Load existing SSO settings when modal opens
@ -196,7 +211,7 @@ const SSOModals: React.FC<SSOModalsProps> = ({
// Enhanced form submission handler
const handleFormSubmit = async (formValues: Record<string, any>) => {
if (!accessToken) {
NotificationsManager.fromBackend("No access token available");
NotificationsManager.fromBackend(t("ssoModals.noAccessToken"));
return;
}
@ -254,14 +269,14 @@ const SSOModals: React.FC<SSOModalsProps> = ({
// Continue with the original flow (show instructions)
handleShowInstructions(formValues);
} catch (error: unknown) {
NotificationsManager.fromBackend("Failed to save SSO settings: " + parseErrorMessage(error));
NotificationsManager.fromBackend(t("ssoModals.saveSettingsFailed", { error: parseErrorMessage(error) }));
}
};
// Handle clearing SSO settings
const handleClearSSO = async () => {
if (!accessToken) {
NotificationsManager.fromBackend("No access token available");
NotificationsManager.fromBackend(t("ssoModals.noAccessToken"));
return;
}
@ -295,10 +310,10 @@ const SSOModals: React.FC<SSOModalsProps> = ({
// Close the main SSO modal and trigger refresh
handleAddSSOOk();
NotificationsManager.success("SSO settings cleared successfully");
NotificationsManager.success(t("ssoModals.clearSettingsSuccess"));
} catch (error) {
console.error("Failed to clear SSO settings:", error);
NotificationsManager.fromBackend("Failed to clear SSO settings");
NotificationsManager.fromBackend(t("ssoModals.clearSettingsFailed"));
}
};
@ -307,22 +322,26 @@ const SSOModals: React.FC<SSOModalsProps> = ({
const config = ssoProviderConfigs[provider];
if (!config) return null;
return config.fields.map((field) => (
<Form.Item
key={field.name}
label={field.label}
name={field.name}
rules={[{ required: true, message: `Please enter the ${field.label.toLowerCase()}` }]}
>
{field.name.includes("client") ? <Input.Password /> : <TextInput placeholder={field.placeholder} />}
</Form.Item>
));
return config.fields.map((field) => {
const labelKey = fieldLabelKeys[field.name];
const label = labelKey ? t(labelKey) : field.label;
return (
<Form.Item
key={field.name}
label={label}
name={field.name}
rules={[{ required: true, message: t("ssoModals.fieldRequired", { label: label.toLowerCase() }) }]}
>
{field.name.includes("client") ? <Input.Password /> : <TextInput placeholder={field.placeholder} />}
</Form.Item>
);
});
};
return (
<>
<Modal
title={ssoConfigured ? "Edit SSO Settings" : "Add SSO"}
title={ssoConfigured ? t("ssoModals.editTitle") : t("ssoModals.addTitle")}
open={isAddSSOModalVisible}
width={800}
footer={null}
@ -338,9 +357,9 @@ const SSOModals: React.FC<SSOModalsProps> = ({
>
<>
<Form.Item
label="SSO Provider"
label={t("ssoModals.ssoProviderLabel")}
name="sso_provider"
rules={[{ required: true, message: "Please select an SSO provider" }]}
rules={[{ required: true, message: t("ssoModals.ssoProviderRequired") }]}
>
<Select>
{Object.entries(ssoProviderLogoMap).map(([value, logo]) => (
@ -376,27 +395,27 @@ const SSOModals: React.FC<SSOModalsProps> = ({
</Form.Item>
<Form.Item
label="Proxy Admin Email"
label={t("ssoModals.proxyAdminEmailLabel")}
name="user_email"
rules={[{ required: true, message: "Please enter the email of the proxy admin" }]}
rules={[{ required: true, message: t("ssoModals.proxyAdminEmailRequired") }]}
>
<TextInput />
</Form.Item>
<Form.Item
label="Proxy Base URL"
label={t("ssoModals.proxyBaseUrlLabel")}
name="proxy_base_url"
normalize={(value) => value?.trim()}
rules={[
{ required: true, message: "Please enter the proxy base url" },
{ required: true, message: t("ssoModals.proxyBaseUrlRequired") },
{
pattern: /^https?:\/\/.+/,
message: "URL must start with http:// or https://",
message: t("ssoModals.proxyBaseUrlInvalidScheme"),
},
{
validator: (_, value) => {
// Only check for trailing slash if the URL starts with http:// or https://
if (value && /^https?:\/\/.+/.test(value) && value.endsWith("/")) {
return Promise.reject("URL must not end with a trailing slash");
return Promise.reject(t("ssoModals.proxyBaseUrlTrailingSlash"));
}
return Promise.resolve();
},
@ -413,7 +432,11 @@ const SSOModals: React.FC<SSOModalsProps> = ({
{({ getFieldValue }) => {
const provider = getFieldValue("sso_provider");
return provider === "okta" || provider === "generic" ? (
<Form.Item label="Use Role Mappings" name="use_role_mappings" valuePropName="checked">
<Form.Item
label={t("ssoModals.useRoleMappingsLabel")}
name="use_role_mappings"
valuePropName="checked"
>
<Checkbox />
</Form.Item>
) : null;
@ -430,9 +453,9 @@ const SSOModals: React.FC<SSOModalsProps> = ({
const useRoleMappings = getFieldValue("use_role_mappings");
return useRoleMappings ? (
<Form.Item
label="Group Claim"
label={t("ssoModals.groupClaimLabel")}
name="group_claim"
rules={[{ required: true, message: "Please enter the group claim" }]}
rules={[{ required: true, message: t("ssoModals.groupClaimRequired") }]}
>
<TextInput />
</Form.Item>
@ -450,28 +473,28 @@ const SSOModals: React.FC<SSOModalsProps> = ({
const useRoleMappings = getFieldValue("use_role_mappings");
return useRoleMappings ? (
<>
<Form.Item label="Default Role" name="default_role" initialValue="Internal User">
<Form.Item label={t("ssoModals.defaultRoleLabel")} name="default_role" initialValue="Internal User">
<Select>
<Select.Option value="internal_user_viewer">Internal Viewer</Select.Option>
<Select.Option value="internal_user">Internal User</Select.Option>
<Select.Option value="proxy_admin_viewer">Admin Viewer</Select.Option>
<Select.Option value="proxy_admin">Proxy Admin</Select.Option>
<Select.Option value="internal_user_viewer">{t("ssoModals.roleInternalViewer")}</Select.Option>
<Select.Option value="internal_user">{t("ssoModals.roleInternalUser")}</Select.Option>
<Select.Option value="proxy_admin_viewer">{t("ssoModals.roleAdminViewer")}</Select.Option>
<Select.Option value="proxy_admin">{t("ssoModals.roleProxyAdmin")}</Select.Option>
</Select>
</Form.Item>
<Form.Item label="Proxy Admin Teams" name="proxy_admin_teams">
<Form.Item label={t("ssoModals.proxyAdminTeamsLabel")} name="proxy_admin_teams">
<TextInput />
</Form.Item>
<Form.Item label="Admin Viewer Teams" name="admin_viewer_teams">
<Form.Item label={t("ssoModals.adminViewerTeamsLabel")} name="admin_viewer_teams">
<TextInput />
</Form.Item>
<Form.Item label="Internal User Teams" name="internal_user_teams">
<Form.Item label={t("ssoModals.internalUserTeamsLabel")} name="internal_user_teams">
<TextInput />
</Form.Item>
<Form.Item label="Internal Viewer Teams" name="internal_viewer_teams">
<Form.Item label={t("ssoModals.internalViewerTeamsLabel")} name="internal_viewer_teams">
<TextInput />
</Form.Item>
</>
@ -506,22 +529,22 @@ const SSOModals: React.FC<SSOModalsProps> = ({
e.currentTarget.style.borderColor = "#6366f1";
}}
>
Clear
{t("common.clear")}
</Button2>
)}
<Button2 htmlType="submit">Save</Button2>
<Button2 htmlType="submit">{t("common.save")}</Button2>
</div>
</Form>
</Modal>
{/* Clear Confirmation Modal */}
<Modal
title="Confirm Clear SSO Settings"
title={t("ssoModals.clearConfirmTitle")}
open={isClearConfirmModalVisible}
onOk={handleClearSSO}
onCancel={() => setIsClearConfirmModalVisible(false)}
okText="Yes, Clear"
cancelText="Cancel"
okText={t("ssoModals.clearConfirmOk")}
cancelText={t("common.cancel")}
okButtonProps={{
danger: true,
style: {
@ -530,25 +553,25 @@ const SSOModals: React.FC<SSOModalsProps> = ({
},
}}
>
<p>Are you sure you want to clear all SSO settings? This action cannot be undone.</p>
<p>Users will no longer be able to login using SSO after this change.</p>
<p>{t("ssoModals.clearConfirmBody1")}</p>
<p>{t("ssoModals.clearConfirmBody2")}</p>
</Modal>
<Modal
title="SSO Setup Instructions"
title={t("ssoModals.instructionsTitle")}
open={isInstructionsModalVisible}
width={800}
footer={null}
onOk={handleInstructionsOk}
onCancel={handleInstructionsCancel}
>
<p>Follow these steps to complete the SSO setup:</p>
<Text className="mt-2">1. DO NOT Exit this TAB</Text>
<Text className="mt-2">2. Open a new tab, visit your proxy base url</Text>
<Text className="mt-2">3. Confirm your SSO is configured correctly and you can login on the new Tab</Text>
<Text className="mt-2">4. If Step 3 is successful, you can close this tab</Text>
<p>{t("ssoModals.instructionsIntro")}</p>
<Text className="mt-2">{t("ssoModals.instructionStep1")}</Text>
<Text className="mt-2">{t("ssoModals.instructionStep2")}</Text>
<Text className="mt-2">{t("ssoModals.instructionStep3")}</Text>
<Text className="mt-2">{t("ssoModals.instructionStep4")}</Text>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button2 onClick={handleInstructionsOk}>Done</Button2>
<Button2 onClick={handleInstructionsOk}>{t("common.done")}</Button2>
</div>
</Modal>
</>

View file

@ -6,6 +6,7 @@ import BudgetDurationDropdown, { getBudgetDurationLabel } from "./common_compone
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
import NotificationsManager from "./molecules/notifications_manager";
import { ModelSelect } from "./ModelSelect/ModelSelect";
import { useTranslation } from "react-i18next";
const { Title, Text } = Typography;
@ -52,7 +53,10 @@ const SettingRow: React.FC<SettingRowProps> = ({ label, description, isEditing,
</Row>
);
const NotSet = () => <Text className="text-gray-400 italic">Not set</Text>;
const NotSet = () => {
const { t } = useTranslation();
return <Text className="text-gray-400 italic">{t("teamSsoSettings.notSet")}</Text>;
};
const renderTags = (values: string[], displayFn?: (v: string) => string) => {
if (!values || values.length === 0) return <NotSet />;
@ -86,6 +90,7 @@ const DEFAULT_VALUES: SettingsValues = {
};
const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken }) => {
const { t } = useTranslation();
const [loading, setLoading] = useState<boolean>(true);
const [values, setValues] = useState<SettingsValues>(DEFAULT_VALUES);
const [isEditing, setIsEditing] = useState<boolean>(false);
@ -108,7 +113,7 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken }) => {
} catch (error) {
console.error("Error fetching team SSO settings:", error);
setFetchError(true);
NotificationsManager.fromBackend("Failed to fetch team settings");
NotificationsManager.fromBackend(t("teamSsoSettings.fetchFailed"));
} finally {
setLoading(false);
}
@ -127,10 +132,10 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken }) => {
setValues(newValues);
setEditedValues(newValues);
setIsEditing(false);
NotificationsManager.success("Default team settings updated successfully");
NotificationsManager.success(t("teamSsoSettings.saveSuccess"));
} catch (error) {
console.error("Error updating team settings:", error);
NotificationsManager.fromBackend("Failed to update team settings");
NotificationsManager.fromBackend(t("teamSsoSettings.saveFailed"));
} finally {
setSaving(false);
}
@ -156,7 +161,7 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken }) => {
if (fetchError) {
return (
<Card>
<Text>No team settings available or you do not have permission to view them.</Text>
<Text>{t("teamSsoSettings.noSettings")}</Text>
</Card>
);
}
@ -167,25 +172,23 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken }) => {
<div className="flex justify-between items-start mb-2">
<div>
<Title level={3} className="m-0 text-gray-900">
Default Team Settings
{t("teamSsoSettings.title")}
</Title>
<Text className="text-gray-500 mt-1 block">
These settings will be applied by default when creating new teams.
</Text>
<Text className="text-gray-500 mt-1 block">{t("teamSsoSettings.subtitle")}</Text>
</div>
<div>
{isEditing ? (
<div className="flex gap-3">
<Button onClick={handleCancel} disabled={saving}>
Cancel
{t("common.cancel")}
</Button>
<Button type="primary" onClick={handleSave} loading={saving} icon={<SaveOutlined />}>
Save Changes
{t("teamSsoSettings.saveChanges")}
</Button>
</div>
) : (
<Button onClick={() => setIsEditing(true)} icon={<EditOutlined />}>
Edit Settings
{t("teamSsoSettings.editSettings")}
</Button>
)}
</div>
@ -194,11 +197,13 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken }) => {
<div className="mt-8">
{/* Budget & Rate Limits */}
<div className="mb-8">
<div className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">Budget & Rate Limits</div>
<div className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">
{t("teamSsoSettings.sectionBudgetRateLimits")}
</div>
<div className="border-t border-gray-100">
<SettingRow
label="Max Budget"
description="Maximum budget (in USD) for new automatically created teams."
label={t("teamSsoSettings.maxBudgetLabel")}
description={t("teamSsoSettings.maxBudgetDesc")}
isEditing={isEditing}
viewContent={
values.max_budget != null ? <Text>${Number(values.max_budget).toLocaleString()}</Text> : <NotSet />
@ -209,7 +214,7 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken }) => {
style={{ maxWidth: 320 }}
value={editedValues.max_budget}
onChange={(v) => update("max_budget", v)}
placeholder="Not set"
placeholder={t("teamSsoSettings.notSetPlaceholder")}
prefix="$"
min={0}
/>
@ -217,8 +222,8 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken }) => {
/>
<SettingRow
label="Budget Duration"
description="How frequently the team's budget resets."
label={t("teamSsoSettings.budgetDurationLabel")}
description={t("teamSsoSettings.budgetDurationDesc")}
isEditing={isEditing}
viewContent={
values.budget_duration ? <Text>{getBudgetDurationLabel(values.budget_duration)}</Text> : <NotSet />
@ -233,8 +238,8 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken }) => {
/>
<SettingRow
label="TPM Limit"
description="Maximum tokens per minute allowed across all models."
label={t("teamSsoSettings.tpmLimitLabel")}
description={t("teamSsoSettings.tpmLimitDesc")}
isEditing={isEditing}
viewContent={values.tpm_limit != null ? <Text>{values.tpm_limit.toLocaleString()}</Text> : <NotSet />}
editContent={
@ -243,15 +248,15 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken }) => {
style={{ maxWidth: 320 }}
value={editedValues.tpm_limit}
onChange={(v) => update("tpm_limit", v)}
placeholder="Not set"
placeholder={t("teamSsoSettings.notSetPlaceholder")}
min={0}
/>
}
/>
<SettingRow
label="RPM Limit"
description="Maximum requests per minute allowed across all models."
label={t("teamSsoSettings.rpmLimitLabel")}
description={t("teamSsoSettings.rpmLimitDesc")}
isEditing={isEditing}
viewContent={values.rpm_limit != null ? <Text>{values.rpm_limit.toLocaleString()}</Text> : <NotSet />}
editContent={
@ -260,7 +265,7 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken }) => {
style={{ maxWidth: 320 }}
value={editedValues.rpm_limit}
onChange={(v) => update("rpm_limit", v)}
placeholder="Not set"
placeholder={t("teamSsoSettings.notSetPlaceholder")}
min={0}
/>
}
@ -270,11 +275,13 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken }) => {
{/* Access & Permissions */}
<div className="mb-8">
<div className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">Access & Permissions</div>
<div className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-2">
{t("teamSsoSettings.sectionAccessPermissions")}
</div>
<div className="border-t border-gray-100">
<SettingRow
label="Models"
description="Default list of models that new teams can access."
label={t("teamSsoSettings.modelsLabel")}
description={t("teamSsoSettings.modelsDesc")}
isEditing={isEditing}
viewContent={renderTags(values.models, getModelDisplayName)}
editContent={
@ -289,8 +296,8 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken }) => {
/>
<SettingRow
label="Team Member Permissions"
description="Default permissions granted to members of newly created teams. /key/info and /key/health are always included."
label={t("teamSsoSettings.teamMemberPermissionsLabel")}
description={t("teamSsoSettings.teamMemberPermissionsDesc")}
isEditing={isEditing}
viewContent={renderTags(values.team_member_permissions)}
editContent={
@ -299,7 +306,7 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken }) => {
style={{ width: "100%" }}
value={editedValues.team_member_permissions || []}
onChange={(v) => update("team_member_permissions", v)}
placeholder="Select permissions"
placeholder={t("teamSsoSettings.selectPermissionsPlaceholder")}
tagRender={({ label, closable, onClose }) => (
<Tag color="blue" closable={closable} onClose={onClose} className="mr-1 mt-1 mb-1">
{label}

View file

@ -20,6 +20,7 @@ import {
type ToolPolicyOverrideRow,
} from "@/components/networking";
import type { Team } from "@/components/key_team_helpers/key_list";
import { useTranslation } from "react-i18next";
interface ToolDetailProps {
toolName: string;
@ -45,6 +46,7 @@ function getDefaultLogsDateRange(): { start: string; end: string } {
}
export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const [overrideSaving, setOverrideSaving] = useState(false);
const [inputPolicySaving, setInputPolicySaving] = useState(false);
@ -145,12 +147,12 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
await updateToolPolicy(accessToken, toolName, { input_policy: newPolicy });
invalidateDetail();
} catch (e: unknown) {
alert(`Failed to update input policy: ${e instanceof Error ? e.message : String(e)}`);
alert(t("toolDetail.updateInputPolicyFailed", { error: e instanceof Error ? e.message : String(e) }));
} finally {
setInputPolicySaving(false);
}
},
[accessToken, toolName, invalidateDetail],
[accessToken, toolName, invalidateDetail, t],
);
const handleOutputPolicyChange = useCallback(
@ -161,12 +163,12 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
await updateToolPolicy(accessToken, toolName, { output_policy: newPolicy });
invalidateDetail();
} catch (e: unknown) {
alert(`Failed to update output policy: ${e instanceof Error ? e.message : String(e)}`);
alert(t("toolDetail.updateOutputPolicyFailed", { error: e instanceof Error ? e.message : String(e) }));
} finally {
setOutputPolicySaving(false);
}
},
[accessToken, toolName, invalidateDetail],
[accessToken, toolName, invalidateDetail, t],
);
const handleAddOverride = useCallback(async () => {
@ -190,11 +192,11 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
setBlockTeamId(null);
setBlockKey(null);
} catch (e: unknown) {
alert(`Failed to add override: ${e instanceof Error ? e.message : String(e)}`);
alert(t("toolDetail.addOverrideFailed", { error: e instanceof Error ? e.message : String(e) }));
} finally {
setOverrideSaving(false);
}
}, [accessToken, toolName, blockScope, blockTeamId, blockKey, invalidateDetail]);
}, [accessToken, toolName, blockScope, blockTeamId, blockKey, invalidateDetail, t]);
const handleRemoveOverride = useCallback(
async (override: ToolPolicyOverrideRow) => {
@ -207,12 +209,12 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
});
invalidateDetail();
} catch (e: unknown) {
alert(`Failed to remove override: ${e instanceof Error ? e.message : String(e)}`);
alert(t("toolDetail.removeOverrideFailed", { error: e instanceof Error ? e.message : String(e) }));
} finally {
setOverrideSaving(false);
}
},
[accessToken, toolName, invalidateDetail],
[accessToken, toolName, invalidateDetail, t],
);
if (detailLoading && !detail) {
@ -227,9 +229,9 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
return (
<div>
<Button type="link" icon={<ArrowLeftOutlined />} onClick={onBack} className="pl-0 mb-4">
Back to Tool Policies
{t("toolDetail.backToToolPolicies")}
</Button>
<p className="text-red-600">Failed to load tool details.</p>
<p className="text-red-600">{t("toolDetail.failedToLoad")}</p>
</div>
);
}
@ -247,7 +249,7 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
<div>
<div className="mb-6">
<Button type="link" icon={<ArrowLeftOutlined />} onClick={onBack} className="pl-0 mb-4">
Back to Tool Policies
{t("toolDetail.backToToolPolicies")}
</Button>
<div className="flex items-start justify-between">
@ -259,13 +261,13 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
{tool.origin ?? "—"}
</span>
<span className="inline-flex items-center px-2.5 py-1 text-xs font-medium rounded-md bg-indigo-50 text-indigo-700 border border-indigo-200">
{(tool.call_count ?? 0).toLocaleString()} calls
{t("toolDetail.callCount", { count: tool.call_count ?? 0 })}
</span>
</div>
<dl className="mt-3 flex flex-wrap gap-x-6 gap-y-1 text-sm text-gray-600">
{tool.user_agent && (
<div className="flex items-center gap-1.5">
<dt className="font-medium text-gray-500 whitespace-nowrap">User Agent:</dt>
<dt className="font-medium text-gray-500 whitespace-nowrap">{t("toolDetail.userAgentLabel")}</dt>
<dd className="font-mono truncate max-w-[40ch]" title={tool.user_agent}>
{tool.user_agent}
</dd>
@ -273,13 +275,15 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
)}
{tool.created_at && (
<div className="flex items-center gap-1.5">
<dt className="font-medium text-gray-500 whitespace-nowrap">First Discovered:</dt>
<dt className="font-medium text-gray-500 whitespace-nowrap">
{t("toolDetail.firstDiscoveredLabel")}
</dt>
<dd>{new Date(tool.created_at).toLocaleString()}</dd>
</div>
)}
{tool.last_used_at && (
<div className="flex items-center gap-1.5">
<dt className="font-medium text-gray-500 whitespace-nowrap">Last Used:</dt>
<dt className="font-medium text-gray-500 whitespace-nowrap">{t("toolDetail.lastUsedLabel")}</dt>
<dd>{new Date(tool.last_used_at).toLocaleString()}</dd>
</div>
)}
@ -292,10 +296,8 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
{/* Two-panel policy layout */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<section className="bg-white rounded-lg border border-gray-200 p-5 shadow-sm">
<h2 className="text-sm font-semibold text-gray-700 mb-1">Input Policy</h2>
<p className="text-xs text-gray-500 mb-3">
{inputDesc ?? "Controls what data this tool is allowed to accept."}
</p>
<h2 className="text-sm font-semibold text-gray-700 mb-1">{t("toolDetail.inputPolicyTitle")}</h2>
<p className="text-xs text-gray-500 mb-3">{inputDesc ?? t("toolDetail.inputPolicyDefaultDesc")}</p>
<PolicySelect
value={tool.input_policy}
toolName={tool.tool_name}
@ -309,10 +311,8 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
</section>
<section className="bg-white rounded-lg border border-gray-200 p-5 shadow-sm">
<h2 className="text-sm font-semibold text-gray-700 mb-1">Output Policy</h2>
<p className="text-xs text-gray-500 mb-3">
{outputDesc ?? "Controls how this tool's output is trusted by downstream tools."}
</p>
<h2 className="text-sm font-semibold text-gray-700 mb-1">{t("toolDetail.outputPolicyTitle")}</h2>
<p className="text-xs text-gray-500 mb-3">{outputDesc ?? t("toolDetail.outputPolicyDefaultDesc")}</p>
<PolicySelect
value={tool.output_policy}
toolName={tool.tool_name}
@ -328,14 +328,14 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
{overrides.length > 0 && (
<section className="bg-white rounded-lg border border-gray-200 p-5 shadow-sm">
<h2 className="text-sm font-semibold text-gray-700 mb-3">Blocked for team or key</h2>
<h2 className="text-sm font-semibold text-gray-700 mb-3">{t("toolDetail.blockedForTitle")}</h2>
<ul className="border rounded-md divide-y divide-gray-100 bg-red-50/30">
{overrides.map((ov) => (
<li key={ov.override_id} className="flex items-center justify-between px-3 py-2.5 text-sm">
<span className="text-gray-700">
{ov.team_id ? `Team: ${ov.team_id}` : ""}
{ov.team_id ? t("toolDetail.teamLabel", { id: ov.team_id }) : ""}
{ov.team_id && ov.key_hash ? " · " : ""}
{ov.key_hash ? `Key: ${ov.key_alias || ov.key_hash.substring(0, 8)}` : ""}
{ov.key_hash ? t("toolDetail.keyLabel", { id: ov.key_alias || ov.key_hash.substring(0, 8) }) : ""}
{!ov.team_id && !ov.key_hash ? "—" : ""}
</span>
<Button
@ -345,7 +345,7 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
disabled={overrideSaving}
onClick={() => handleRemoveOverride(ov)}
>
Remove
{t("common.remove")}
</Button>
</li>
))}
@ -354,10 +354,10 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
)}
<section className="bg-white rounded-lg border border-gray-200 p-5 shadow-sm">
<h2 className="text-sm font-semibold text-gray-700 mb-3">Block for team or key</h2>
<h2 className="text-sm font-semibold text-gray-700 mb-3">{t("toolDetail.blockForTitle")}</h2>
<div className="flex flex-col gap-4 max-w-md">
<div>
<span className="text-sm font-medium text-gray-700 block mb-2">Scope</span>
<span className="text-sm font-medium text-gray-700 block mb-2">{t("toolDetail.scopeLabel")}</span>
<div className="flex items-center gap-6">
<label className="flex items-center gap-2 cursor-pointer text-sm text-gray-700">
<input
@ -366,7 +366,7 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
onChange={() => setBlockScope("team")}
className="align-middle"
/>
Team
{t("toolDetail.scopeTeam")}
</label>
<label className="flex items-center gap-2 cursor-pointer text-sm text-gray-700">
<input
@ -375,19 +375,19 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
onChange={() => setBlockScope("key")}
className="align-middle"
/>
Key
{t("toolDetail.scopeKey")}
</label>
</div>
</div>
<div>
<span className="text-sm font-medium text-gray-700 block mb-2">
{blockScope === "team" ? "Team" : "Key"}
{blockScope === "team" ? t("toolDetail.scopeTeam") : t("toolDetail.scopeKey")}
</span>
{blockScope === "team" ? (
<TeamDropdown value={blockTeamId ?? undefined} onChange={(id) => setBlockTeamId(id || null)} />
) : (
<Select
placeholder="Select key"
placeholder={t("toolDetail.selectKeyPlaceholder")}
allowClear
showSearch
optionFilterProp="label"
@ -412,7 +412,9 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
loading={overrideSaving}
onClick={handleAddOverride}
>
Block for {blockScope}
{t("toolDetail.blockForButton", {
scope: blockScope === "team" ? t("toolDetail.scopeTeam") : t("toolDetail.scopeKey"),
})}
</Button>
</div>
</section>
@ -420,7 +422,7 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
<section className="bg-white rounded-lg border border-gray-200 p-5 shadow-sm">
<h2 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
<HistoryOutlined />
Recent logs
{t("toolDetail.recentLogsTitle")}
</h2>
<LogViewer
guardrailName={tool.tool_name}

View file

@ -712,6 +712,132 @@
"deleteTeamFailed": "Error deleting the team: {{error}}",
"createTeamFailed": "Error creating the team: {{error}}"
},
"scim": {
"title": "SCIM Configuration",
"description": "System for Cross-domain Identity Management (SCIM) allows you to automatically provision and manage users and groups in LiteLLM.",
"tenantUrlTitle": "SCIM Tenant URL",
"tenantUrlDesc": "Use this URL in your identity provider SCIM integration settings.",
"urlCopied": "URL copied to clipboard",
"authTokenTitle": "Authentication Token",
"usingScimCalloutTitle": "Using SCIM",
"calloutDesc": "You need a SCIM token to authenticate with the SCIM API. Create one below and use it in your SCIM provider configuration.",
"tokenNameLabel": "Token Name",
"tokenNameRequired": "Please enter a name for your token",
"tokenNamePlaceholder": "SCIM Access Token",
"createTokenButton": "Create SCIM Token",
"yourScimTokenTitle": "Your SCIM Token",
"tokenWarning": "Make sure to copy this token now. You will not be able to see it again.",
"tokenCopied": "Token copied to clipboard",
"createAnotherToken": "Create Another Token",
"needToBeLoggedIn": "You need to be logged in to create a SCIM token",
"createTokenSuccess": "SCIM token created successfully",
"createTokenFailed": "Failed to create SCIM token: {{error}}"
},
"ssoModals": {
"editTitle": "Edit SSO Settings",
"addTitle": "Add SSO",
"noAccessToken": "No access token available",
"saveSettingsFailed": "Failed to save SSO settings: {{error}}",
"clearSettingsSuccess": "SSO settings cleared successfully",
"clearSettingsFailed": "Failed to clear SSO settings",
"ssoProviderLabel": "SSO Provider",
"ssoProviderRequired": "Please select an SSO provider",
"proxyAdminEmailLabel": "Proxy Admin Email",
"proxyAdminEmailRequired": "Please enter the email of the proxy admin",
"proxyBaseUrlLabel": "Proxy Base URL",
"proxyBaseUrlRequired": "Please enter the proxy base url",
"proxyBaseUrlInvalidScheme": "URL must start with http:// or https://",
"proxyBaseUrlTrailingSlash": "URL must not end with a trailing slash",
"useRoleMappingsLabel": "Use Role Mappings",
"groupClaimLabel": "Group Claim",
"groupClaimRequired": "Please enter the group claim",
"defaultRoleLabel": "Default Role",
"roleInternalViewer": "Internal Viewer",
"roleInternalUser": "Internal User",
"roleAdminViewer": "Admin Viewer",
"roleProxyAdmin": "Proxy Admin",
"proxyAdminTeamsLabel": "Proxy Admin Teams",
"adminViewerTeamsLabel": "Admin Viewer Teams",
"internalUserTeamsLabel": "Internal User Teams",
"internalViewerTeamsLabel": "Internal Viewer Teams",
"clearConfirmTitle": "Confirm Clear SSO Settings",
"clearConfirmOk": "Yes, Clear",
"clearConfirmBody1": "Are you sure you want to clear all SSO settings? This action cannot be undone.",
"clearConfirmBody2": "Users will no longer be able to login using SSO after this change.",
"instructionsTitle": "SSO Setup Instructions",
"instructionsIntro": "Follow these steps to complete the SSO setup:",
"instructionStep1": "1. DO NOT Exit this TAB",
"instructionStep2": "2. Open a new tab, visit your proxy base url",
"instructionStep3": "3. Confirm your SSO is configured correctly and you can login on the new Tab",
"instructionStep4": "4. If Step 3 is successful, you can close this tab",
"fieldRequired": "Please enter the {{label}}",
"fieldLabels": {
"googleClientId": "Google Client ID",
"googleClientSecret": "Google Client Secret",
"microsoftClientId": "Microsoft Client ID",
"microsoftClientSecret": "Microsoft Client Secret",
"microsoftTenant": "Microsoft Tenant",
"genericClientId": "Generic Client ID",
"genericClientSecret": "Generic Client Secret",
"authorizationEndpoint": "Authorization Endpoint",
"tokenEndpoint": "Token Endpoint",
"userinfoEndpoint": "Userinfo Endpoint"
}
},
"teamSsoSettings": {
"title": "Default Team Settings",
"subtitle": "These settings will be applied by default when creating new teams.",
"editSettings": "Edit Settings",
"saveChanges": "Save Changes",
"noSettings": "No team settings available or you do not have permission to view them.",
"notSet": "Not set",
"notSetPlaceholder": "Not set",
"fetchFailed": "Failed to fetch team settings",
"saveSuccess": "Default team settings updated successfully",
"saveFailed": "Failed to update team settings",
"sectionBudgetRateLimits": "Budget & Rate Limits",
"sectionAccessPermissions": "Access & Permissions",
"maxBudgetLabel": "Max Budget",
"maxBudgetDesc": "Maximum budget (in USD) for new automatically created teams.",
"budgetDurationLabel": "Budget Duration",
"budgetDurationDesc": "How frequently the team's budget resets.",
"tpmLimitLabel": "TPM Limit",
"tpmLimitDesc": "Maximum tokens per minute allowed across all models.",
"rpmLimitLabel": "RPM Limit",
"rpmLimitDesc": "Maximum requests per minute allowed across all models.",
"modelsLabel": "Models",
"modelsDesc": "Default list of models that new teams can access.",
"teamMemberPermissionsLabel": "Team Member Permissions",
"teamMemberPermissionsDesc": "Default permissions granted to members of newly created teams. /key/info and /key/health are always included.",
"selectPermissionsPlaceholder": "Select permissions"
},
"toolDetail": {
"backToToolPolicies": "Back to Tool Policies",
"failedToLoad": "Failed to load tool details.",
"callCount_one": "{{count}} call",
"callCount_other": "{{count}} calls",
"userAgentLabel": "User Agent:",
"firstDiscoveredLabel": "First Discovered:",
"lastUsedLabel": "Last Used:",
"inputPolicyTitle": "Input Policy",
"inputPolicyDefaultDesc": "Controls what data this tool is allowed to accept.",
"outputPolicyTitle": "Output Policy",
"outputPolicyDefaultDesc": "Controls how this tool's output is trusted by downstream tools.",
"blockedForTitle": "Blocked for team or key",
"teamLabel": "Team: {{id}}",
"keyLabel": "Key: {{id}}",
"blockForTitle": "Block for team or key",
"scopeLabel": "Scope",
"scopeTeam": "Team",
"scopeKey": "Key",
"selectKeyPlaceholder": "Select key",
"blockForButton": "Block for {{scope}}",
"recentLogsTitle": "Recent logs",
"updateInputPolicyFailed": "Failed to update input policy: {{error}}",
"updateOutputPolicyFailed": "Failed to update output policy: {{error}}",
"addOverrideFailed": "Failed to add override: {{error}}",
"removeOverrideFailed": "Failed to remove override: {{error}}"
},
"common": {
"save": "Save",
"cancel": "Cancel",

View file

@ -712,6 +712,132 @@
"deleteTeamFailed": "删除团队失败:{{error}}",
"createTeamFailed": "创建团队失败:{{error}}"
},
"scim": {
"title": "SCIM 配置",
"description": "跨域身份管理系统(SCIM)允许您自动在 LiteLLM 中预配和管理用户及用户组。",
"tenantUrlTitle": "SCIM 租户 URL",
"tenantUrlDesc": "在您的身份提供商 SCIM 集成设置中使用此 URL。",
"urlCopied": "URL 已复制到剪贴板",
"authTokenTitle": "认证 Token",
"usingScimCalloutTitle": "使用 SCIM",
"calloutDesc": "您需要一个 SCIM Token 来验证 SCIM API 身份。在下方创建一个,并在 SCIM 提供商配置中使用。",
"tokenNameLabel": "Token 名称",
"tokenNameRequired": "请输入 Token 名称",
"tokenNamePlaceholder": "SCIM 访问 Token",
"createTokenButton": "创建 SCIM Token",
"yourScimTokenTitle": "您的 SCIM Token",
"tokenWarning": "请立即复制此 Token,此后将无法再次查看。",
"tokenCopied": "Token 已复制到剪贴板",
"createAnotherToken": "创建另一个 Token",
"needToBeLoggedIn": "您需要登录后才能创建 SCIM Token",
"createTokenSuccess": "SCIM Token 创建成功",
"createTokenFailed": "创建 SCIM Token 失败:{{error}}"
},
"ssoModals": {
"editTitle": "编辑 SSO 设置",
"addTitle": "添加 SSO",
"noAccessToken": "无可用访问令牌",
"saveSettingsFailed": "保存 SSO 设置失败:{{error}}",
"clearSettingsSuccess": "SSO 设置已成功清除",
"clearSettingsFailed": "清除 SSO 设置失败",
"ssoProviderLabel": "SSO 提供商",
"ssoProviderRequired": "请选择 SSO 提供商",
"proxyAdminEmailLabel": "代理管理员邮箱",
"proxyAdminEmailRequired": "请输入代理管理员的邮箱",
"proxyBaseUrlLabel": "代理基础 URL",
"proxyBaseUrlRequired": "请输入代理基础 URL",
"proxyBaseUrlInvalidScheme": "URL 必须以 http:// 或 https:// 开头",
"proxyBaseUrlTrailingSlash": "URL 末尾不能有斜杠",
"useRoleMappingsLabel": "使用角色映射",
"groupClaimLabel": "Group Claim",
"groupClaimRequired": "请输入 Group Claim",
"defaultRoleLabel": "默认角色",
"roleInternalViewer": "内部查看者",
"roleInternalUser": "内部用户",
"roleAdminViewer": "管理查看者",
"roleProxyAdmin": "代理管理员",
"proxyAdminTeamsLabel": "代理管理员团队",
"adminViewerTeamsLabel": "管理查看者团队",
"internalUserTeamsLabel": "内部用户团队",
"internalViewerTeamsLabel": "内部查看者团队",
"clearConfirmTitle": "确认清除 SSO 设置",
"clearConfirmOk": "是,清除",
"clearConfirmBody1": "确定要清除所有 SSO 设置吗?此操作无法撤销。",
"clearConfirmBody2": "此更改后,用户将无法再使用 SSO 登录。",
"instructionsTitle": "SSO 设置说明",
"instructionsIntro": "请按以下步骤完成 SSO 设置:",
"instructionStep1": "1. 不要关闭此标签页",
"instructionStep2": "2. 打开新标签页,访问您的代理基础 URL",
"instructionStep3": "3. 确认 SSO 配置正确,且可以在新标签页上登录",
"instructionStep4": "4. 若第 3 步成功,可以关闭此标签页",
"fieldRequired": "请输入{{label}}",
"fieldLabels": {
"googleClientId": "Google Client ID",
"googleClientSecret": "Google Client Secret",
"microsoftClientId": "Microsoft Client ID",
"microsoftClientSecret": "Microsoft Client Secret",
"microsoftTenant": "Microsoft 租户",
"genericClientId": "通用 Client ID",
"genericClientSecret": "通用 Client Secret",
"authorizationEndpoint": "授权端点",
"tokenEndpoint": "Token 端点",
"userinfoEndpoint": "用户信息端点"
}
},
"teamSsoSettings": {
"title": "默认团队设置",
"subtitle": "创建新团队时将默认应用这些设置。",
"editSettings": "编辑设置",
"saveChanges": "保存更改",
"noSettings": "暂无团队设置,或您没有查看权限。",
"notSet": "未设置",
"notSetPlaceholder": "未设置",
"fetchFailed": "获取团队设置失败",
"saveSuccess": "默认团队设置已成功更新",
"saveFailed": "更新团队设置失败",
"sectionBudgetRateLimits": "预算与速率限制",
"sectionAccessPermissions": "访问与权限",
"maxBudgetLabel": "最大预算",
"maxBudgetDesc": "新自动创建团队的最大预算(USD)。",
"budgetDurationLabel": "预算周期",
"budgetDurationDesc": "团队预算的重置频率。",
"tpmLimitLabel": "TPM 限制",
"tpmLimitDesc": "所有模型每分钟最大 Token 数。",
"rpmLimitLabel": "RPM 限制",
"rpmLimitDesc": "所有模型每分钟最大请求数。",
"modelsLabel": "模型",
"modelsDesc": "新团队可访问的默认模型列表。",
"teamMemberPermissionsLabel": "团队成员权限",
"teamMemberPermissionsDesc": "新创建团队的成员默认获得的权限。/key/info 和 /key/health 始终包含。",
"selectPermissionsPlaceholder": "选择权限"
},
"toolDetail": {
"backToToolPolicies": "返回工具策略",
"failedToLoad": "加载工具详情失败。",
"callCount_one": "{{count}} 次调用",
"callCount_other": "{{count}} 次调用",
"userAgentLabel": "User Agent:",
"firstDiscoveredLabel": "首次发现:",
"lastUsedLabel": "最近使用:",
"inputPolicyTitle": "输入策略",
"inputPolicyDefaultDesc": "控制此工具允许接受的数据。",
"outputPolicyTitle": "输出策略",
"outputPolicyDefaultDesc": "控制此工具的输出如何被下游工具信任。",
"blockedForTitle": "已为团队或密钥屏蔽",
"teamLabel": "团队:{{id}}",
"keyLabel": "密钥:{{id}}",
"blockForTitle": "为团队或密钥屏蔽",
"scopeLabel": "范围",
"scopeTeam": "团队",
"scopeKey": "密钥",
"selectKeyPlaceholder": "选择密钥",
"blockForButton": "屏蔽 {{scope}}",
"recentLogsTitle": "最近日志",
"updateInputPolicyFailed": "更新输入策略失败:{{error}}",
"updateOutputPolicyFailed": "更新输出策略失败:{{error}}",
"addOverrideFailed": "添加覆盖规则失败:{{error}}",
"removeOverrideFailed": "移除覆盖规则失败:{{error}}"
},
"common": {
"save": "保存",
"cancel": "取消",