feat(ui): i18n for organizations, pass-through info, and onboarding link

This commit is contained in:
吹雪 2026-06-10 13:43:39 +09:00
parent 7c0faa8bc6
commit 38f2137eca
6 changed files with 385 additions and 121 deletions

View file

@ -1,5 +1,6 @@
import React from "react";
import { Text } from "@tremor/react";
import { useTranslation } from "react-i18next";
import VectorStorePermissions from "./permissions/VectorStorePermissions";
import MCPServerPermissions from "./permissions/MCPServerPermissions";
import AgentPermissions from "./permissions/AgentPermissions";
@ -29,6 +30,7 @@ export function ObjectPermissionsView({
className = "",
accessToken,
}: ObjectPermissionsViewProps) {
const { t } = useTranslation();
const vectorStores = objectPermission?.vector_stores || [];
const mcpServers = objectPermission?.mcp_servers || [];
const mcpAccessGroups = objectPermission?.mcp_access_groups || [];
@ -50,10 +52,10 @@ export function ObjectPermissionsView({
/>
<AgentPermissions agents={agents} agentAccessGroups={agentAccessGroups} accessToken={accessToken} />
<div className="rounded-md border border-gray-100 p-4">
<Text className="text-sm font-medium text-gray-800">Search tools</Text>
<Text className="text-sm font-medium text-gray-800">{t("objectPermissionsView.searchToolsTitle")}</Text>
{searchTools.length === 0 ? (
<Text className="mt-1 block text-xs text-gray-500">
No restriction — all configured search tools are allowed for this team.
{t("objectPermissionsView.searchToolsNoRestriction")}
</Text>
) : (
<Text className="mt-1 block text-xs text-gray-700">{searchTools.join(", ")}</Text>
@ -67,8 +69,8 @@ export function ObjectPermissionsView({
<div className={`bg-white border border-gray-200 rounded-lg p-6 ${className}`}>
<div className="flex items-center gap-2 mb-6">
<div>
<Text className="font-semibold text-gray-900">Object Permissions</Text>
<Text className="text-xs text-gray-500">Access control for Vector Stores and MCP Servers</Text>
<Text className="font-semibold text-gray-900">{t("objectPermissionsView.title")}</Text>
<Text className="text-xs text-gray-500">{t("objectPermissionsView.subtitle")}</Text>
</div>
</div>
{content}
@ -78,7 +80,7 @@ export function ObjectPermissionsView({
return (
<div className={`${className}`}>
<Text className="font-medium text-gray-900 mb-3">Object Permissions</Text>
<Text className="font-medium text-gray-900 mb-3">{t("objectPermissionsView.title")}</Text>
{content}
</div>
);

View file

@ -2,6 +2,7 @@ import React from "react";
import { Button, Modal, Typography } from "antd";
import { CopyToClipboard } from "react-copy-to-clipboard";
import { Text } from "@tremor/react";
import { useTranslation } from "react-i18next";
import NotificationsManager from "./molecules/notifications_manager";
export interface InvitationLink {
@ -32,6 +33,7 @@ export default function OnboardingModal({
invitationLinkData,
modalType = "invitation",
}: OnboardingProps) {
const { t } = useTranslation();
const { Title, Paragraph } = Typography;
const handleInvitationOk = () => {
setIsInvitationLinkModalVisible(false);
@ -62,7 +64,11 @@ export default function OnboardingModal({
return (
<Modal
title={modalType === "invitation" ? "Invitation Link" : "Reset Password Link"}
title={
modalType === "invitation"
? t("onboardingLink.invitationLinkTitle")
: t("onboardingLink.resetPasswordLinkTitle")
}
open={isInvitationLinkModalVisible}
width={800}
footer={null}
@ -71,23 +77,29 @@ export default function OnboardingModal({
>
<Paragraph>
{modalType === "invitation"
? "Copy and send the generated link to onboard this user to the proxy."
: "Copy and send the generated link to the user to reset their password."}
? t("onboardingLink.invitationLinkDesc")
: t("onboardingLink.resetPasswordLinkDesc")}
</Paragraph>
<div className="flex justify-between pt-5 pb-2">
<Text className="text-base">User ID</Text>
<Text className="text-base">{t("onboardingLink.userId")}</Text>
<Text>{invitationLinkData?.user_id}</Text>
</div>
<div className="flex justify-between pt-5 pb-2">
<Text>{modalType === "invitation" ? "Invitation Link" : "Reset Password Link"}</Text>
<Text>
{modalType === "invitation"
? t("onboardingLink.invitationLinkTitle")
: t("onboardingLink.resetPasswordLinkTitle")}
</Text>
<Text>
<Text>{getInvitationUrl()}</Text>
</Text>
</div>
<div className="flex justify-end mt-5">
<CopyToClipboard text={getInvitationUrl()} onCopy={() => NotificationsManager.success("Copied!")}>
<CopyToClipboard text={getInvitationUrl()} onCopy={() => NotificationsManager.success(t("common.copied"))}>
<Button type="primary">
{modalType === "invitation" ? "Copy invitation link" : "Copy password reset link"}
{modalType === "invitation"
? t("onboardingLink.copyInvitationLink")
: t("onboardingLink.copyPasswordResetLink")}
</Button>
</CopyToClipboard>
</div>

View file

@ -1,5 +1,6 @@
import OrganizationFilters, { FilterState } from "@/app/(dashboard)/organizations/OrganizationFilters";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Trans, useTranslation } from "react-i18next";
import { ChevronDownIcon, ChevronRightIcon, RefreshIcon } from "@heroicons/react/outline";
import {
Badge,
@ -71,6 +72,7 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
setOrganizations,
premiumUser,
}) => {
const { t } = useTranslation();
const [selectedOrgId, setSelectedOrgId] = useState<string | null>(null);
const [editOrg, setEditOrg] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@ -138,7 +140,7 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
try {
setIsDeleting(true);
await organizationDeleteCall(accessToken, orgToDelete);
NotificationsManager.success("Organization deleted successfully");
NotificationsManager.success(t("organizations.deleteSuccess"));
setIsDeleteModalOpen(false);
setOrgToDelete(null);
@ -186,7 +188,7 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
}
await organizationCreateCall(accessToken, values);
NotificationsManager.success("Organization created successfully");
NotificationsManager.success(t("organizations.createSuccess"));
setIsOrgModalVisible(false);
form.resetFields();
// Refresh organizations list
@ -205,11 +207,12 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
return (
<div>
<Text>
This is a LiteLLM Enterprise feature, and requires a valid key to use. Get a trial key{" "}
<a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer">
here
</a>
.
<Trans
i18nKey="organizations.enterpriseFeature"
components={{
pricingLink: <a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer" />,
}}
/>
</Text>
</div>
);
@ -221,7 +224,7 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
<Col numColSpan={1} className="flex flex-col gap-2">
{(userRole === "Admin" || userRole === "Org Admin") && (
<Button className="w-fit" onClick={() => setIsOrgModalVisible(true)}>
+ Create New Organization
{t("organizations.createNewOrg")}
</Button>
)}
{selectedOrgId ? (
@ -241,10 +244,10 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
<TabGroup className="gap-2 h-[75vh] w-full">
<TabList className="flex justify-between mt-2 w-full items-center">
<div className="flex">
<Tab>Your Organizations</Tab>
<Tab>{t("organizations.tabYourOrgs")}</Tab>
</div>
<div className="flex items-center space-x-2">
{lastRefreshed && <Text>Last Refreshed: {lastRefreshed}</Text>}
{lastRefreshed && <Text>{t("organizations.lastRefreshed", { time: lastRefreshed })}</Text>}
<Icon
icon={RefreshIcon}
variant="shadow"
@ -256,7 +259,7 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
</TabList>
<TabPanels>
<TabPanel>
<Text>Click on &ldquo;Organization ID&rdquo; to view organization details.</Text>
<Text>{t("organizations.clickOrgIdHint")}</Text>
<Grid numItems={1} className="gap-2 pt-2 pb-2 h-[75vh] w-full mt-2">
<Col numColSpan={1}>
<Card className="w-full mx-auto flex-auto overflow-hidden overflow-y-auto max-h-[50vh]">
@ -274,15 +277,15 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Organization ID</TableHeaderCell>
<TableHeaderCell>Organization Name</TableHeaderCell>
<TableHeaderCell>Created</TableHeaderCell>
<TableHeaderCell>Spend (USD)</TableHeaderCell>
<TableHeaderCell>Budget (USD)</TableHeaderCell>
<TableHeaderCell>Models</TableHeaderCell>
<TableHeaderCell>TPM / RPM Limits</TableHeaderCell>
<TableHeaderCell>Info</TableHeaderCell>
<TableHeaderCell>Actions</TableHeaderCell>
<TableHeaderCell>{t("organizations.colOrgId")}</TableHeaderCell>
<TableHeaderCell>{t("organizations.colOrgName")}</TableHeaderCell>
<TableHeaderCell>{t("common.createdAt")}</TableHeaderCell>
<TableHeaderCell>{t("organizations.colSpend")}</TableHeaderCell>
<TableHeaderCell>{t("organizations.colBudget")}</TableHeaderCell>
<TableHeaderCell>{t("organizations.colModels")}</TableHeaderCell>
<TableHeaderCell>{t("organizations.colTpmRpmLimits")}</TableHeaderCell>
<TableHeaderCell>{t("organizations.colInfo")}</TableHeaderCell>
<TableHeaderCell>{t("common.actions")}</TableHeaderCell>
</TableRow>
</TableHead>
@ -309,14 +312,16 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
</TableCell>
<TableCell>{org.organization_alias}</TableCell>
<TableCell>
{org.created_at ? new Date(org.created_at).toLocaleDateString() : "N/A"}
{org.created_at
? new Date(org.created_at).toLocaleDateString()
: t("organizations.notAvailable")}
</TableCell>
<TableCell>{formatNumberWithCommas(org.spend, 4)}</TableCell>
<TableCell>
{org.litellm_budget_table?.max_budget !== null &&
org.litellm_budget_table?.max_budget !== undefined
? org.litellm_budget_table?.max_budget
: "No limit"}
: t("organizations.noLimit")}
</TableCell>
<TableCell
style={{
@ -331,7 +336,7 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
<div className="flex flex-col">
{org.models.length === 0 ? (
<Badge size={"xs"} className="mb-1" color="red">
<Text>All Proxy Models</Text>
<Text>{t("organizations.allProxyModels")}</Text>
</Badge>
) : (
<>
@ -360,7 +365,7 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
{org.models.slice(0, 3).map((model, index) =>
model === "all-proxy-models" ? (
<Badge key={index} size={"xs"} color="red">
<Text>All Proxy Models</Text>
<Text>{t("organizations.allProxyModels")}</Text>
</Badge>
) : (
<Badge key={index} size={"xs"} color="blue">
@ -376,10 +381,9 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
!expandedAccordions[org.organization_id || ""] && (
<Badge size={"xs"} color="gray" className="cursor-pointer">
<Text>
+{org.models.length - 3}{" "}
{org.models.length - 3 === 1
? "more model"
: "more models"}
{t("organizations.moreModels", {
count: org.models.length - 3,
})}
</Text>
</Badge>
)}
@ -388,7 +392,7 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
{org.models.slice(3).map((model, index) =>
model === "all-proxy-models" ? (
<Badge key={index + 3} size={"xs"} color="red">
<Text>All Proxy Models</Text>
<Text>{t("organizations.allProxyModels")}</Text>
</Badge>
) : (
<Badge key={index + 3} size={"xs"} color="blue">
@ -412,26 +416,28 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
</TableCell>
<TableCell>
<Text>
TPM:{" "}
{t("organizations.tpmLabel")}:{" "}
{org.litellm_budget_table?.tpm_limit
? org.litellm_budget_table?.tpm_limit
: "Unlimited"}
: t("organizations.unlimited")}
<br />
RPM:{" "}
{t("organizations.rpmLabel")}:{" "}
{org.litellm_budget_table?.rpm_limit
? org.litellm_budget_table?.rpm_limit
: "Unlimited"}
: t("organizations.unlimited")}
</Text>
</TableCell>
<TableCell>
<Text>{org.members?.length || 0} Members</Text>
<Text>
{t("organizations.memberCount", { count: org.members?.length || 0 })}
</Text>
</TableCell>
<TableCell>
{userRole === "Admin" && (
<>
<TableIconActionButton
variant="Edit"
tooltipText="Edit organization"
tooltipText={t("organizations.editOrgTooltip")}
onClick={() => {
setSelectedOrgId(org.organization_id);
setEditOrg(true);
@ -439,7 +445,7 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
/>
<TableIconActionButton
variant="Delete"
tooltipText="Delete organization"
tooltipText={t("organizations.deleteOrgTooltip")}
onClick={() => handleDelete(org.organization_id)}
/>
</>
@ -459,21 +465,27 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
)}
</Col>
</Grid>
<Modal title="Create Organization" visible={isOrgModalVisible} width={800} footer={null} onCancel={handleCancel}>
<Modal
title={t("organizations.createModalTitle")}
visible={isOrgModalVisible}
width={800}
footer={null}
onCancel={handleCancel}
>
<Form form={form} onFinish={handleCreate} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} labelAlign="left">
<Form.Item
label="Organization Name"
label={t("organizations.formOrgName")}
name="organization_alias"
rules={[
{
required: true,
message: "Please input an organization name",
message: t("organizations.formOrgNameRequired"),
},
]}
>
<TextInput placeholder="" />
</Form.Item>
<Form.Item label="Models" name="models">
<Form.Item label={t("organizations.formModels")} name="models">
<ModelSelect
options={{ showAllProxyModelsOverride: true, includeSpecialOptions: true }}
value={form.getFieldValue("models")}
@ -482,81 +494,81 @@ const OrganizationsTable: React.FC<OrganizationsTableProps> = ({
/>
</Form.Item>
<Form.Item label="Max Budget (USD)" name="max_budget">
<Form.Item label={t("organizations.formMaxBudget")} name="max_budget">
<NumericalInput step={0.01} precision={2} width={200} />
</Form.Item>
<Form.Item label="Reset Budget" name="budget_duration">
<Form.Item label={t("organizations.formResetBudget")} name="budget_duration">
<Select2 defaultValue={null} placeholder="n/a">
<Select2.Option value="24h">daily</Select2.Option>
<Select2.Option value="7d">weekly</Select2.Option>
<Select2.Option value="30d">monthly</Select2.Option>
<Select2.Option value="24h">{t("commonComponents.durationSelect.daily")}</Select2.Option>
<Select2.Option value="7d">{t("commonComponents.durationSelect.weekly")}</Select2.Option>
<Select2.Option value="30d">{t("commonComponents.durationSelect.monthly")}</Select2.Option>
</Select2>
</Form.Item>
<Form.Item label="Tokens per minute Limit (TPM)" name="tpm_limit">
<Form.Item label={t("organizations.formTpmLimit")} name="tpm_limit">
<NumericalInput step={1} width={400} />
</Form.Item>
<Form.Item label="Requests per minute Limit (RPM)" name="rpm_limit">
<Form.Item label={t("organizations.formRpmLimit")} name="rpm_limit">
<NumericalInput step={1} width={400} />
</Form.Item>
<Form.Item
label={
<span>
Allowed Vector Stores{" "}
<Tooltip title="Select which vector stores this organization can access by default. Leave empty for access to all vector stores">
{t("organizations.formAllowedVectorStores")}{" "}
<Tooltip title={t("organizations.formAllowedVectorStoresTooltip")}>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="allowed_vector_store_ids"
className="mt-4"
help="Select vector stores this organization can access. Leave empty for access to all vector stores"
help={t("organizations.formAllowedVectorStoresHelp")}
>
<VectorStoreSelector
onChange={(values) => form.setFieldValue("allowed_vector_store_ids", values)}
value={form.getFieldValue("allowed_vector_store_ids")}
accessToken={accessToken || ""}
placeholder="Select vector stores (optional)"
placeholder={t("organizations.formAllowedVectorStoresPlaceholder")}
/>
</Form.Item>
<Form.Item
label={
<span>
Allowed MCP Servers{" "}
<Tooltip title="Select which MCP servers and access groups this organization can access by default.">
{t("organizations.formAllowedMcpServers")}{" "}
<Tooltip title={t("organizations.formAllowedMcpServersTooltip")}>
<InfoCircleOutlined style={{ marginLeft: "4px" }} />
</Tooltip>
</span>
}
name="allowed_mcp_servers_and_groups"
className="mt-4"
help="Select MCP servers and access groups this organization can access."
help={t("organizations.formAllowedMcpServersHelp")}
>
<MCPServerSelector
onChange={(values) => form.setFieldValue("allowed_mcp_servers_and_groups", values)}
value={form.getFieldValue("allowed_mcp_servers_and_groups")}
accessToken={accessToken || ""}
placeholder="Select MCP servers and access groups (optional)"
placeholder={t("organizations.formAllowedMcpServersPlaceholder")}
/>
</Form.Item>
<Form.Item label="Metadata" name="metadata">
<Form.Item label={t("organizations.formMetadata")} name="metadata">
<Input.TextArea rows={4} />
</Form.Item>
<div style={{ textAlign: "right", marginTop: "10px" }}>
<Button type="submit">Create Organization</Button>
<Button type="submit">{t("organizations.createModalTitle")}</Button>
</div>
</Form>
</Modal>
<DeleteResourceModal
isOpen={isDeleteModalOpen}
title="Delete Organization?"
message="Are you sure you want to delete this organization? This action cannot be undone."
resourceInformationTitle="Organization Information"
resourceInformation={[{ label: "Organization ID", value: orgToDelete, code: true }]}
title={t("organizations.deleteModalTitle")}
message={t("organizations.deleteModalMessage")}
resourceInformationTitle={t("organizations.deleteModalResourceTitle")}
resourceInformation={[{ label: t("organizations.colOrgId"), value: orgToDelete, code: true }]}
onCancel={cancelDelete}
onOk={confirmDelete}
confirmLoading={isDeleting}

View file

@ -14,6 +14,7 @@ import {
TextInput,
} from "@tremor/react";
import { Button, Form, Input, Switch, InputNumber, Select } from "antd";
import { useTranslation } from "react-i18next";
import { updatePassThroughEndpoint, deletePassThroughEndpointsCall } from "./networking";
import { Eye, EyeOff } from "lucide-react";
import RoutePreview from "./route_preview";
@ -70,6 +71,7 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
premiumUser = false,
onEndpointUpdated,
}) => {
const { t } = useTranslation();
const [endpointData, setEndpointData] = useState<PassThroughEndpoint | null>(initialEndpointData);
const [loading, setLoading] = useState(false);
const [isEditing, setIsEditing] = useState(false);
@ -90,7 +92,7 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
try {
headers = typeof values.headers === "string" ? JSON.parse(values.headers) : values.headers;
} catch (e) {
NotificationsManager.fromBackend("Invalid JSON format for headers");
NotificationsManager.fromBackend(t("passThroughInfo.invalidJsonHeaders"));
return;
}
}
@ -120,7 +122,7 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
}
} catch (error) {
console.error("Error updating endpoint:", error);
NotificationsManager.fromBackend("Failed to update pass through endpoint");
NotificationsManager.fromBackend(t("passThroughInfo.updateFailed"));
}
};
@ -129,23 +131,23 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
if (!accessToken || !endpointData?.id) return;
await deletePassThroughEndpointsCall(accessToken, endpointData.id);
NotificationsManager.success("Pass through endpoint deleted successfully");
NotificationsManager.success(t("passThroughInfo.deleteSuccess"));
onClose();
if (onEndpointUpdated) {
onEndpointUpdated();
}
} catch (error) {
console.error("Error deleting endpoint:", error);
NotificationsManager.fromBackend("Failed to delete pass through endpoint");
NotificationsManager.fromBackend(t("passThroughInfo.deleteFailed"));
}
};
if (loading) {
return <div className="p-4">Loading...</div>;
return <div className="p-4">{t("common.loading")}</div>;
}
if (!endpointData) {
return <div className="p-4">Pass through endpoint not found</div>;
return <div className="p-4">{t("passThroughInfo.notFound")}</div>;
}
return (
@ -153,17 +155,17 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
<div className="flex justify-between items-center mb-6">
<div>
<Button onClick={onClose} className="mb-4">
← Back
{t("passThroughInfo.backButton")}
</Button>
<Title>Pass Through Endpoint: {endpointData.path}</Title>
<Title>{t("passThroughInfo.pageTitle", { path: endpointData.path })}</Title>
<Text className="text-gray-500 font-mono">{endpointData.id}</Text>
</div>
</div>
<TabGroup>
<TabList className="mb-4">
<Tab key="overview">Overview</Tab>
{isAdmin ? <Tab key="settings">Settings</Tab> : <></>}
<Tab key="overview">{t("passThroughInfo.tabOverview")}</Tab>
{isAdmin ? <Tab key="settings">{t("common.settings")}</Tab> : <></>}
</TabList>
<TabPanels>
@ -171,35 +173,37 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
<TabPanel>
<Grid numItems={1} numItemsSm={2} numItemsLg={3} className="gap-6">
<Card>
<Text>Path</Text>
<Text>{t("passThroughInfo.labelPath")}</Text>
<div className="mt-2">
<Title className="font-mono">{endpointData.path}</Title>
</div>
</Card>
<Card>
<Text>Target</Text>
<Text>{t("passThroughInfo.labelTarget")}</Text>
<div className="mt-2">
<Title>{endpointData.target}</Title>
</div>
</Card>
<Card>
<Text>Configuration</Text>
<Text>{t("passThroughInfo.labelConfiguration")}</Text>
<div className="mt-2 space-y-2">
<div>
<Badge color={endpointData.include_subpath ? "green" : "gray"}>
{endpointData.include_subpath ? "Include Subpath" : "Exact Path"}
{endpointData.include_subpath
? t("passThroughInfo.includeSubpath")
: t("passThroughInfo.exactPath")}
</Badge>
</div>
<div>
<Badge color={endpointData.auth ? "blue" : "gray"}>
{endpointData.auth ? "Auth Required" : "No Auth"}
{endpointData.auth ? t("passThroughInfo.authRequired") : t("passThroughInfo.noAuth")}
</Badge>
</div>
{endpointData.methods && endpointData.methods.length > 0 && (
<div>
<Text className="text-xs text-gray-500">HTTP Methods:</Text>
<Text className="text-xs text-gray-500">{t("passThroughInfo.httpMethods")}</Text>
<div className="flex flex-wrap gap-1 mt-1">
{endpointData.methods.map((method) => (
<Badge key={method} color="indigo" size="sm">
@ -211,12 +215,12 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
)}
{(!endpointData.methods || endpointData.methods.length === 0) && (
<div>
<Text className="text-xs text-gray-500">All HTTP methods supported</Text>
<Text className="text-xs text-gray-500">{t("passThroughInfo.allHttpMethodsSupported")}</Text>
</div>
)}
{endpointData.cost_per_request !== undefined && (
<div>
<Text>Cost per request: ${endpointData.cost_per_request}</Text>
<Text>{t("passThroughInfo.costPerRequest", { cost: endpointData.cost_per_request })}</Text>
</div>
)}
</div>
@ -235,8 +239,10 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
{endpointData.headers && Object.keys(endpointData.headers).length > 0 && (
<Card className="mt-6">
<div className="flex justify-between items-center">
<Text className="font-medium">Headers</Text>
<Badge color="blue">{Object.keys(endpointData.headers).length} headers configured</Badge>
<Text className="font-medium">{t("passThroughInfo.labelHeaders")}</Text>
<Badge color="blue">
{t("passThroughInfo.headersConfigured", { count: Object.keys(endpointData.headers).length })}
</Badge>
</div>
<div className="mt-4">
<PasswordField value={endpointData.headers} />
@ -247,8 +253,10 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
{endpointData.guardrails && Object.keys(endpointData.guardrails).length > 0 && (
<Card className="mt-6">
<div className="flex justify-between items-center">
<Text className="font-medium">Guardrails</Text>
<Badge color="purple">{Object.keys(endpointData.guardrails).length} guardrails configured</Badge>
<Text className="font-medium">{t("passThroughInfo.labelGuardrails")}</Text>
<Badge color="purple">
{t("passThroughInfo.guardrailsConfigured", { count: Object.keys(endpointData.guardrails).length })}
</Badge>
</div>
<div className="mt-4 space-y-2">
{Object.entries(endpointData.guardrails).map(([name, settings]) => (
@ -256,13 +264,21 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
<div className="font-medium text-sm">{name}</div>
{settings && (settings.request_fields || settings.response_fields) && (
<div className="mt-2 text-xs text-gray-600 space-y-1">
{settings.request_fields && <div>Request fields: {settings.request_fields.join(", ")}</div>}
{settings.request_fields && (
<div>
{t("passThroughInfo.requestFields", { fields: settings.request_fields.join(", ") })}
</div>
)}
{settings.response_fields && (
<div>Response fields: {settings.response_fields.join(", ")}</div>
<div>
{t("passThroughInfo.responseFields", { fields: settings.response_fields.join(", ") })}
</div>
)}
</div>
)}
{!settings && <div className="text-xs text-gray-600 mt-1">Uses entire payload</div>}
{!settings && (
<div className="text-xs text-gray-600 mt-1">{t("passThroughInfo.usesEntirePayload")}</div>
)}
</div>
))}
</div>
@ -275,13 +291,15 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
<TabPanel>
<Card>
<div className="flex justify-between items-center mb-4">
<Title>Pass Through Endpoint Settings</Title>
<Title>{t("passThroughInfo.settingsTitle")}</Title>
<div className="space-x-2">
{!isEditing && (
<>
<TremorButton onClick={() => setIsEditing(true)}>Edit Settings</TremorButton>
<TremorButton onClick={() => setIsEditing(true)}>
{t("passThroughInfo.editSettings")}
</TremorButton>
<TremorButton onClick={handleDeleteEndpoint} variant="secondary" color="red">
Delete Endpoint
{t("passThroughInfo.deleteEndpoint")}
</TremorButton>
</>
)}
@ -303,14 +321,14 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
layout="vertical"
>
<Form.Item
label="Target URL"
label={t("passThroughInfo.formTargetUrl")}
name="target"
rules={[{ required: true, message: "Please input a target URL" }]}
rules={[{ required: true, message: t("passThroughInfo.formTargetUrlRequired") }]}
>
<TextInput placeholder="https://api.example.com" />
</Form.Item>
<Form.Item label="Headers (JSON)" name="headers">
<Form.Item label={t("passThroughInfo.formHeadersJson")} name="headers">
<Input.TextArea
rows={5}
placeholder='{"Authorization": "Bearer your-token", "Content-Type": "application/json"}'
@ -318,17 +336,17 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
</Form.Item>
<Form.Item
label="HTTP Methods (Optional)"
label={t("passThroughInfo.formHttpMethods")}
name="methods"
extra={
selectedMethods.length === 0
? "All HTTP methods supported (default)"
: `Only ${selectedMethods.join(", ")} requests will be routed to this endpoint`
? t("passThroughInfo.allHttpMethodsDefault")
: t("passThroughInfo.selectedMethodsDesc", { methods: selectedMethods.join(", ") })
}
>
<Select
mode="multiple"
placeholder="Select methods (leave empty for all)"
placeholder={t("passThroughInfo.selectMethodsPlaceholder")}
value={selectedMethods}
onChange={setSelectedMethods}
allowClear
@ -342,11 +360,15 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
</Select>
</Form.Item>
<Form.Item label="Include Subpath" name="include_subpath" valuePropName="checked">
<Form.Item
label={t("passThroughInfo.formIncludeSubpath")}
name="include_subpath"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item label="Cost per Request" name="cost_per_request">
<Form.Item label={t("passThroughInfo.formCostPerRequest")} name="cost_per_request">
<InputNumber min={0} step={0.01} precision={2} placeholder="0.00" addonBefore="$" />
</Form.Item>
@ -368,44 +390,46 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
</div>
<div className="flex justify-end gap-2 mt-6">
<Button onClick={() => setIsEditing(false)}>Cancel</Button>
<TremorButton>Save Changes</TremorButton>
<Button onClick={() => setIsEditing(false)}>{t("common.cancel")}</Button>
<TremorButton>{t("passThroughInfo.saveChanges")}</TremorButton>
</div>
</Form>
) : (
<div className="space-y-4">
<div>
<Text className="font-medium">Path</Text>
<Text className="font-medium">{t("passThroughInfo.labelPath")}</Text>
<div className="font-mono">{endpointData.path}</div>
</div>
<div>
<Text className="font-medium">Target URL</Text>
<Text className="font-medium">{t("passThroughInfo.formTargetUrl")}</Text>
<div>{endpointData.target}</div>
</div>
<div>
<Text className="font-medium">Include Subpath</Text>
<Text className="font-medium">{t("passThroughInfo.formIncludeSubpath")}</Text>
<Badge color={endpointData.include_subpath ? "green" : "gray"}>
{endpointData.include_subpath ? "Yes" : "No"}
{endpointData.include_subpath ? t("common.yes") : t("common.no")}
</Badge>
</div>
{endpointData.cost_per_request !== undefined && (
<div>
<Text className="font-medium">Cost per Request</Text>
<Text className="font-medium">{t("passThroughInfo.formCostPerRequest")}</Text>
<div>${endpointData.cost_per_request}</div>
</div>
)}
<div>
<Text className="font-medium">Authentication Required</Text>
<Badge color={endpointData.auth ? "green" : "gray"}>{endpointData.auth ? "Yes" : "No"}</Badge>
<Text className="font-medium">{t("passThroughInfo.authenticationRequired")}</Text>
<Badge color={endpointData.auth ? "green" : "gray"}>
{endpointData.auth ? t("common.yes") : t("common.no")}
</Badge>
</div>
<div>
<Text className="font-medium">Headers</Text>
<Text className="font-medium">{t("passThroughInfo.labelHeaders")}</Text>
{endpointData.headers && Object.keys(endpointData.headers).length > 0 ? (
<div className="mt-2">
<PasswordField value={endpointData.headers} />
</div>
) : (
<div className="text-gray-500">No headers configured</div>
<div className="text-gray-500">{t("passThroughInfo.noHeadersConfigured")}</div>
)}
</div>
</div>

View file

@ -1466,5 +1466,112 @@
"createdAt": "Created At",
"updatedAt": "Updated At",
"comingSoon": "Coming soon"
},
"objectPermissionsView": {
"title": "Object Permissions",
"subtitle": "Access control for Vector Stores and MCP Servers",
"searchToolsTitle": "Search tools",
"searchToolsNoRestriction": "No restriction — all configured search tools are allowed for this team."
},
"onboardingLink": {
"invitationLinkTitle": "Invitation Link",
"resetPasswordLinkTitle": "Reset Password Link",
"invitationLinkDesc": "Copy and send the generated link to onboard this user to the proxy.",
"resetPasswordLinkDesc": "Copy and send the generated link to the user to reset their password.",
"userId": "User ID",
"copyInvitationLink": "Copy invitation link",
"copyPasswordResetLink": "Copy password reset link"
},
"organizations": {
"enterpriseFeature": "This is a LiteLLM Enterprise feature, and requires a valid key to use. Get a trial key <pricingLink>here</pricingLink>.",
"createNewOrg": "+ Create New Organization",
"tabYourOrgs": "Your Organizations",
"lastRefreshed": "Last Refreshed: {{time}}",
"clickOrgIdHint": "Click on \"Organization ID\" to view organization details.",
"colOrgId": "Organization ID",
"colOrgName": "Organization Name",
"colSpend": "Spend (USD)",
"colBudget": "Budget (USD)",
"colModels": "Models",
"colTpmRpmLimits": "TPM / RPM Limits",
"colInfo": "Info",
"notAvailable": "N/A",
"noLimit": "No limit",
"allProxyModels": "All Proxy Models",
"moreModels_one": "+{{count}} more model",
"moreModels_other": "+{{count}} more models",
"tpmLabel": "TPM",
"rpmLabel": "RPM",
"unlimited": "Unlimited",
"memberCount_one": "{{count}} Member",
"memberCount_other": "{{count}} Members",
"editOrgTooltip": "Edit organization",
"deleteOrgTooltip": "Delete organization",
"createModalTitle": "Create Organization",
"formOrgName": "Organization Name",
"formOrgNameRequired": "Please input an organization name",
"formModels": "Models",
"formMaxBudget": "Max Budget (USD)",
"formResetBudget": "Reset Budget",
"formTpmLimit": "Tokens per minute Limit (TPM)",
"formRpmLimit": "Requests per minute Limit (RPM)",
"formAllowedVectorStores": "Allowed Vector Stores",
"formAllowedVectorStoresTooltip": "Select which vector stores this organization can access by default. Leave empty for access to all vector stores",
"formAllowedVectorStoresHelp": "Select vector stores this organization can access. Leave empty for access to all vector stores",
"formAllowedVectorStoresPlaceholder": "Select vector stores (optional)",
"formAllowedMcpServers": "Allowed MCP Servers",
"formAllowedMcpServersTooltip": "Select which MCP servers and access groups this organization can access by default.",
"formAllowedMcpServersHelp": "Select MCP servers and access groups this organization can access.",
"formAllowedMcpServersPlaceholder": "Select MCP servers and access groups (optional)",
"formMetadata": "Metadata",
"deleteModalTitle": "Delete Organization?",
"deleteModalMessage": "Are you sure you want to delete this organization? This action cannot be undone.",
"deleteModalResourceTitle": "Organization Information",
"deleteSuccess": "Organization deleted successfully",
"createSuccess": "Organization created successfully"
},
"passThroughInfo": {
"notFound": "Pass through endpoint not found",
"backButton": "← Back",
"pageTitle": "Pass Through Endpoint: {{path}}",
"tabOverview": "Overview",
"labelPath": "Path",
"labelTarget": "Target",
"labelConfiguration": "Configuration",
"includeSubpath": "Include Subpath",
"exactPath": "Exact Path",
"authRequired": "Auth Required",
"noAuth": "No Auth",
"httpMethods": "HTTP Methods:",
"allHttpMethodsSupported": "All HTTP methods supported",
"costPerRequest": "Cost per request: ${{cost}}",
"labelHeaders": "Headers",
"headersConfigured_one": "{{count}} header configured",
"headersConfigured_other": "{{count}} headers configured",
"labelGuardrails": "Guardrails",
"guardrailsConfigured_one": "{{count}} guardrail configured",
"guardrailsConfigured_other": "{{count}} guardrails configured",
"requestFields": "Request fields: {{fields}}",
"responseFields": "Response fields: {{fields}}",
"usesEntirePayload": "Uses entire payload",
"settingsTitle": "Pass Through Endpoint Settings",
"editSettings": "Edit Settings",
"deleteEndpoint": "Delete Endpoint",
"formTargetUrl": "Target URL",
"formTargetUrlRequired": "Please input a target URL",
"formHeadersJson": "Headers (JSON)",
"formHttpMethods": "HTTP Methods (Optional)",
"allHttpMethodsDefault": "All HTTP methods supported (default)",
"selectedMethodsDesc": "Only {{methods}} requests will be routed to this endpoint",
"selectMethodsPlaceholder": "Select methods (leave empty for all)",
"formIncludeSubpath": "Include Subpath",
"formCostPerRequest": "Cost per Request",
"saveChanges": "Save Changes",
"authenticationRequired": "Authentication Required",
"noHeadersConfigured": "No headers configured",
"invalidJsonHeaders": "Invalid JSON format for headers",
"updateFailed": "Failed to update pass through endpoint",
"deleteSuccess": "Pass through endpoint deleted successfully",
"deleteFailed": "Failed to delete pass through endpoint"
}
}

View file

@ -1466,5 +1466,112 @@
"createdAt": "创建时间",
"updatedAt": "更新时间",
"comingSoon": "即将推出"
},
"objectPermissionsView": {
"title": "对象权限",
"subtitle": "向量存储与 MCP 服务器的访问控制",
"searchToolsTitle": "搜索工具",
"searchToolsNoRestriction": "无限制 — 该团队可使用所有已配置的搜索工具。"
},
"onboardingLink": {
"invitationLinkTitle": "邀请链接",
"resetPasswordLinkTitle": "重置密码链接",
"invitationLinkDesc": "复制并将生成的链接发送给该用户,以便将其接入代理。",
"resetPasswordLinkDesc": "复制并将生成的链接发送给该用户,以便其重置密码。",
"userId": "用户 ID",
"copyInvitationLink": "复制邀请链接",
"copyPasswordResetLink": "复制密码重置链接"
},
"organizations": {
"enterpriseFeature": "这是 LiteLLM 企业版功能,需要有效的密钥才能使用。获取试用密钥请访问<pricingLink>此处</pricingLink>。",
"createNewOrg": "+ 创建新组织",
"tabYourOrgs": "我的组织",
"lastRefreshed": "最后刷新:{{time}}",
"clickOrgIdHint": "点击「组织 ID」可查看组织详情。",
"colOrgId": "组织 ID",
"colOrgName": "组织名称",
"colSpend": "花费(USD)",
"colBudget": "预算(USD)",
"colModels": "模型",
"colTpmRpmLimits": "TPM / RPM 限制",
"colInfo": "信息",
"notAvailable": "N/A",
"noLimit": "无限制",
"allProxyModels": "所有代理模型",
"moreModels_one": "+{{count}} 个模型",
"moreModels_other": "+{{count}} 个模型",
"tpmLabel": "TPM",
"rpmLabel": "RPM",
"unlimited": "无限制",
"memberCount_one": "{{count}} 位成员",
"memberCount_other": "{{count}} 位成员",
"editOrgTooltip": "编辑组织",
"deleteOrgTooltip": "删除组织",
"createModalTitle": "创建组织",
"formOrgName": "组织名称",
"formOrgNameRequired": "请输入组织名称",
"formModels": "模型",
"formMaxBudget": "最大预算(USD)",
"formResetBudget": "重置预算",
"formTpmLimit": "每分钟 Token 限制(TPM)",
"formRpmLimit": "每分钟请求限制(RPM)",
"formAllowedVectorStores": "允许的向量存储",
"formAllowedVectorStoresTooltip": "选择此组织默认可访问的向量存储。留空则允许访问所有向量存储",
"formAllowedVectorStoresHelp": "选择此组织可访问的向量存储。留空则允许访问所有向量存储",
"formAllowedVectorStoresPlaceholder": "选择向量存储(可选)",
"formAllowedMcpServers": "允许的 MCP 服务器",
"formAllowedMcpServersTooltip": "选择此组织默认可访问的 MCP 服务器和访问组。",
"formAllowedMcpServersHelp": "选择此组织可访问的 MCP 服务器和访问组。",
"formAllowedMcpServersPlaceholder": "选择 MCP 服务器和访问组(可选)",
"formMetadata": "元数据",
"deleteModalTitle": "删除组织?",
"deleteModalMessage": "确定要删除此组织吗?此操作无法撤销。",
"deleteModalResourceTitle": "组织信息",
"deleteSuccess": "组织已成功删除",
"createSuccess": "组织已成功创建"
},
"passThroughInfo": {
"notFound": "未找到透传端点",
"backButton": "← 返回",
"pageTitle": "透传端点:{{path}}",
"tabOverview": "概览",
"labelPath": "路径",
"labelTarget": "目标",
"labelConfiguration": "配置",
"includeSubpath": "包含子路径",
"exactPath": "精确路径",
"authRequired": "需要认证",
"noAuth": "无需认证",
"httpMethods": "HTTP 方法:",
"allHttpMethodsSupported": "支持所有 HTTP 方法",
"costPerRequest": "每次请求费用:${{cost}}",
"labelHeaders": "请求头",
"headersConfigured_one": "已配置 {{count}} 个请求头",
"headersConfigured_other": "已配置 {{count}} 个请求头",
"labelGuardrails": "护栏",
"guardrailsConfigured_one": "已配置 {{count}} 个护栏",
"guardrailsConfigured_other": "已配置 {{count}} 个护栏",
"requestFields": "请求字段:{{fields}}",
"responseFields": "响应字段:{{fields}}",
"usesEntirePayload": "检查整个载荷",
"settingsTitle": "透传端点设置",
"editSettings": "编辑设置",
"deleteEndpoint": "删除端点",
"formTargetUrl": "目标 URL",
"formTargetUrlRequired": "请输入目标 URL",
"formHeadersJson": "请求头(JSON)",
"formHttpMethods": "HTTP 方法(可选)",
"allHttpMethodsDefault": "支持所有 HTTP 方法(默认)",
"selectedMethodsDesc": "仅 {{methods}} 请求将路由到此端点",
"selectMethodsPlaceholder": "选择方法(留空表示全部)",
"formIncludeSubpath": "包含子路径",
"formCostPerRequest": "每次请求费用",
"saveChanges": "保存更改",
"authenticationRequired": "需要认证",
"noHeadersConfigured": "未配置请求头",
"invalidJsonHeaders": "请求头的 JSON 格式无效",
"updateFailed": "更新透传端点失败",
"deleteSuccess": "透传端点已成功删除",
"deleteFailed": "删除透传端点失败"
}
}