From 29a97784e77ffecea25b0091089e709bf3f0c817 Mon Sep 17 00:00:00 2001 From: Krish Dholakia Date: Tue, 21 Oct 2025 23:26:43 -0700 Subject: [PATCH] (feat) Passthrough - set auth on passthrough endpoints, on the UI (#15778) * fix(add_pass_through.tsx): allow setting 'auth' to true for passthrough endpoints on the UI * fix: working update auth on passthrough endpoints + show auth on passthrough table --- litellm/proxy/_new_secret_config.yaml | 23 ++++++- litellm/proxy/_types.py | 4 ++ .../ModelsAndEndpointsView.tsx | 1 + ui/litellm-dashboard/src/app/page.tsx | 1 + .../src/components/add_pass_through.tsx | 18 ++++- .../PassThroughSecuritySection.tsx | 66 +++++++++++++++++++ .../src/components/pass_through_info.tsx | 27 ++++++++ .../src/components/pass_through_settings.tsx | 22 ++++++- .../components/templates/model_dashboard.tsx | 1 + 9 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/common_components/PassThroughSecuritySection.tsx diff --git a/litellm/proxy/_new_secret_config.yaml b/litellm/proxy/_new_secret_config.yaml index c0a369cbc25..2a2e6c99407 100644 --- a/litellm/proxy/_new_secret_config.yaml +++ b/litellm/proxy/_new_secret_config.yaml @@ -3,5 +3,24 @@ model_list: litellm_params: model: bedrock/global.anthropic.claude-sonnet-4-5-20250929-v1:0 -litellm_settings: - callbacks: ["otel"] \ No newline at end of file +mcp_servers: + github_mcp: + url: "https://api.githubcopilot.com/mcp" + auth_type: oauth2 + authorization_url: https://github.com/login/oauth/authorize + token_url: https://github.com/login/oauth/access_token + client_id: os.environ/GITHUB_OAUTH_CLIENT_ID + client_secret: os.environ/GITHUB_OAUTH_CLIENT_SECRET + scopes: ["public_repo", "user:email"] + +general_settings: + pass_through_endpoints: + - path: "/fake-openai-proxy-10" # Route on LiteLLM Proxy + target: "https://webhook.site/74bbcc59-a61f-4028-81e2-9e06814e81fe" # Target endpoint + headers: # Headers to forward + Authorization: "bearer sk-1234" + content-type: application/json + accept: application/json + auth: true + include_subpath: true + cost_per_request: 0 diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 226395f5115..36446e65e66 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1628,6 +1628,10 @@ class PassThroughGenericEndpoint(LiteLLMPydanticObjectBase): default=0.0, description="The USD cost per request to the target endpoint. This is used to calculate the cost of the request to the target endpoint.", ) + auth: bool = Field( + default=False, + description="Whether authentication is required for the pass-through endpoint. If True, requests to the endpoint will require a valid LiteLLM API key.", + ) class PassThroughEndpointResponse(LiteLLMPydanticObjectBase): diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx index 42f96129552..8d65c0e1702 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx @@ -734,6 +734,7 @@ const ModelsAndEndpointsView: React.FC = ({ userRole={userRole} userID={userID} modelData={modelData} + premiumUser={premiumUser} /> diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 1860ffcb60d..5dc9fe0509a 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -450,6 +450,7 @@ export default function CreateKeyPage() { userRole={userRole} accessToken={accessToken} modelData={modelData} + premiumUser={premiumUser} /> ) : page == "logs" ? ( >; + premiumUser?: boolean; } const AddPassThroughEndpoint: React.FC = ({ accessToken, setPassThroughItems, passThroughItems, + premiumUser = false, }) => { const [form] = Form.useForm(); const [isModalVisible, setIsModalVisible] = useState(false); @@ -47,7 +50,7 @@ const AddPassThroughEndpoint: React.FC = ({ const [pathValue, setPathValue] = useState(""); const [targetValue, setTargetValue] = useState(""); const [includeSubpath, setIncludeSubpath] = useState(true); - + const [authEnabled, setAuthEnabled] = useState(false); const handleCancel = () => { form.resetFields(); setPathValue(""); @@ -70,6 +73,10 @@ const AddPassThroughEndpoint: React.FC = ({ console.log("addPassThrough called with:", formValues); setIsLoading(true); try { + // Remove auth field if not premium user + if (!premiumUser && 'auth' in formValues) { + delete formValues.auth; + } console.log(`formValues: ${JSON.stringify(formValues)}`); const response = await createPassThroughEndpoint(accessToken, formValues); @@ -233,6 +240,15 @@ const AddPassThroughEndpoint: React.FC = ({ + {/* Security Section */} + { + setAuthEnabled(checked); + form.setFieldsValue({ auth: checked }); + }} + /> {/* Billing Section */} Billing diff --git a/ui/litellm-dashboard/src/components/common_components/PassThroughSecuritySection.tsx b/ui/litellm-dashboard/src/components/common_components/PassThroughSecuritySection.tsx new file mode 100644 index 00000000000..c42094abb55 --- /dev/null +++ b/ui/litellm-dashboard/src/components/common_components/PassThroughSecuritySection.tsx @@ -0,0 +1,66 @@ +import React from "react"; +import { Card, Title, Subtitle, Text } from "@tremor/react"; +import { Form, Switch } from "antd"; + +export interface PassThroughSecuritySectionProps { + premiumUser: boolean; + authEnabled: boolean; + onAuthChange: (checked: boolean) => void; +} + +/** + * Reusable Security section for pass-through endpoints + * Shows authentication toggle for premium users or upgrade message for free users + */ +const PassThroughSecuritySection: React.FC = ({ + premiumUser, + authEnabled, + onAuthChange, +}) => { + return ( + + Security + + When enabled, requests to this endpoint will require a valid LiteLLM API key + + {premiumUser ? ( + + { + onAuthChange(checked); + }} + /> + + ) : ( +
+
+ + Authentication (Premium) +
+
+ + Setting authentication for pass-through endpoints is a LiteLLM Enterprise feature. Get a trial key{" "} + + here + + . + +
+
+ )} +
+ ); +}; + +export default PassThroughSecuritySection; + diff --git a/ui/litellm-dashboard/src/components/pass_through_info.tsx b/ui/litellm-dashboard/src/components/pass_through_info.tsx index 9b9d8aefaa3..c42a710caf2 100644 --- a/ui/litellm-dashboard/src/components/pass_through_info.tsx +++ b/ui/litellm-dashboard/src/components/pass_through_info.tsx @@ -18,12 +18,14 @@ import { updatePassThroughEndpoint, deletePassThroughEndpointsCall } from "./net import { Eye, EyeOff } from "lucide-react"; import RoutePreview from "./route_preview"; import NotificationsManager from "./molecules/notifications_manager"; +import PassThroughSecuritySection from "./common_components/PassThroughSecuritySection"; export interface PassThroughInfoProps { endpointData: PassThroughEndpoint; onClose: () => void; accessToken: string | null; isAdmin: boolean; + premiumUser?: boolean; onEndpointUpdated?: () => void; } @@ -34,6 +36,7 @@ interface PassThroughEndpoint { headers: Record; include_subpath?: boolean; cost_per_request?: number; + auth?: boolean; } // Password field component for headers @@ -58,11 +61,13 @@ const PassThroughInfoView: React.FC = ({ onClose, accessToken, isAdmin, + premiumUser = false, onEndpointUpdated, }) => { const [endpointData, setEndpointData] = useState(initialEndpointData); const [loading, setLoading] = useState(false); const [isEditing, setIsEditing] = useState(false); + const [authEnabled, setAuthEnabled] = useState(initialEndpointData?.auth || false); const [form] = Form.useForm(); const handleEndpointUpdate = async (values: any) => { @@ -86,6 +91,7 @@ const PassThroughInfoView: React.FC = ({ headers: headers, include_subpath: values.include_subpath, cost_per_request: values.cost_per_request, + auth: premiumUser ? values.auth : undefined, }; await updatePassThroughEndpoint(accessToken, endpointData.id, updateData); @@ -174,6 +180,11 @@ const PassThroughInfoView: React.FC = ({ {endpointData.include_subpath ? "Include Subpath" : "Exact Path"} +
+ + {endpointData.auth ? "Auth Required" : "No Auth"} + +
{endpointData.cost_per_request !== undefined && (
Cost per request: ${endpointData.cost_per_request} @@ -232,6 +243,7 @@ const PassThroughInfoView: React.FC = ({ headers: endpointData.headers ? JSON.stringify(endpointData.headers, null, 2) : "", include_subpath: endpointData.include_subpath || false, cost_per_request: endpointData.cost_per_request, + auth: endpointData.auth || false, }} layout="vertical" > @@ -258,6 +270,15 @@ const PassThroughInfoView: React.FC = ({ + { + setAuthEnabled(checked); + form.setFieldsValue({ auth: checked }); + }} + /> +
Save Changes @@ -285,6 +306,12 @@ const PassThroughInfoView: React.FC = ({
${endpointData.cost_per_request}
)} +
+ Authentication Required + + {endpointData.auth ? "Yes" : "No"} + +
Headers {endpointData.headers && Object.keys(endpointData.headers).length > 0 ? ( diff --git a/ui/litellm-dashboard/src/components/pass_through_settings.tsx b/ui/litellm-dashboard/src/components/pass_through_settings.tsx index c21f2e3c0be..6e22533ed14 100644 --- a/ui/litellm-dashboard/src/components/pass_through_settings.tsx +++ b/ui/litellm-dashboard/src/components/pass_through_settings.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect } from "react"; import { Text, Button, Icon, Title } from "@tremor/react"; import { deletePassThroughEndpointsCall, getPassThroughEndpointsCall } from "./networking"; -import { Tooltip } from "antd"; -import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline"; +import { Badge, Tooltip } from "antd"; +import { PencilAltIcon, TrashIcon, InformationCircleIcon } from "@heroicons/react/outline"; import AddPassThroughEndpoint from "./add_pass_through"; import PassThroughInfoView from "./pass_through_info"; import { DataTable } from "./view_logs/table"; @@ -15,6 +15,7 @@ interface GeneralSettingsPageProps { userRole: string | null; userID: string | null; modelData: any; + premiumUser?: boolean; } interface routingStrategyArgs { @@ -37,6 +38,7 @@ export interface passThroughItem { headers: object; include_subpath?: boolean; cost_per_request?: number; + auth?: boolean; } // Password field component for headers @@ -54,7 +56,7 @@ const PasswordField: React.FC<{ value: object }> = ({ value }) => { ); }; -const PassThroughSettings: React.FC = ({ accessToken, userRole, userID, modelData }) => { +const PassThroughSettings: React.FC = ({ accessToken, userRole, userID, modelData, premiumUser }) => { const [generalSettings, setGeneralSettings] = useState([]); const [selectedEndpointId, setSelectedEndpointId] = useState(null); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -144,6 +146,18 @@ const PassThroughSettings: React.FC = ({ accessToken, accessorKey: "target", cell: (info: any) => {info.getValue()}, }, + { + header: () => ( +
+ Authentication + + + +
+ ), + accessorKey: "auth", + cell: (info: any) => {info.getValue() ? "Yes" : "No"}, + }, { header: "Headers", accessorKey: "headers", @@ -192,6 +206,7 @@ const PassThroughSettings: React.FC = ({ accessToken, onClose={() => setSelectedEndpointId(null)} accessToken={accessToken} isAdmin={userRole === "Admin" || userRole === "admin"} + premiumUser={premiumUser} onEndpointUpdated={handleEndpointUpdated} /> ); @@ -208,6 +223,7 @@ const PassThroughSettings: React.FC = ({ accessToken, accessToken={accessToken} setPassThroughItems={setGeneralSettings} passThroughItems={generalSettings} + premiumUser={premiumUser} /> = ({ userRole={userRole} userID={userID} modelData={modelData} + premiumUser={premiumUser} />