mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-27 06:13:07 +00:00
fix(ui): align pass-through headers and authentication defaults (#48081)
This commit is contained in:
parent
22cfee1534
commit
7a75cf273e
7 changed files with 59 additions and 100 deletions
|
|
@ -4,8 +4,6 @@ import PassThroughSettings from "@/components/PassThroughSettings/PassThroughSet
|
|||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
export default function PassThroughPanel() {
|
||||
const { accessToken, userRole, userId: userID, premiumUser } = useAuthorized();
|
||||
return (
|
||||
<PassThroughSettings accessToken={accessToken} userRole={userRole} userID={userID} premiumUser={premiumUser} />
|
||||
);
|
||||
const { accessToken, userRole, userId: userID } = useAuthorized();
|
||||
return <PassThroughSettings accessToken={accessToken} userRole={userRole} userID={userID} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ interface PassThroughSettingsProps {
|
|||
accessToken: string | null;
|
||||
userRole: string | null;
|
||||
userID: string | null;
|
||||
premiumUser?: boolean;
|
||||
}
|
||||
|
||||
export interface passThroughItem {
|
||||
|
|
@ -37,7 +36,7 @@ export interface passThroughItem {
|
|||
is_from_config?: boolean;
|
||||
}
|
||||
|
||||
const PassThroughSettings: React.FC<PassThroughSettingsProps> = ({ accessToken, userRole, userID, premiumUser }) => {
|
||||
const PassThroughSettings: React.FC<PassThroughSettingsProps> = ({ accessToken, userRole, userID }) => {
|
||||
const [generalSettings, setGeneralSettings] = useState<passThroughItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedEndpointId, setSelectedEndpointId] = useState<string | null>(null);
|
||||
|
|
@ -116,7 +115,6 @@ const PassThroughSettings: React.FC<PassThroughSettingsProps> = ({ accessToken,
|
|||
onClose={() => setSelectedEndpointId(null)}
|
||||
accessToken={accessToken}
|
||||
isAdmin={userRole === "Admin" || userRole === "admin"}
|
||||
premiumUser={premiumUser}
|
||||
onEndpointUpdated={handleEndpointUpdated}
|
||||
/>
|
||||
);
|
||||
|
|
@ -133,7 +131,6 @@ const PassThroughSettings: React.FC<PassThroughSettingsProps> = ({ accessToken,
|
|||
accessToken={accessToken}
|
||||
setPassThroughItems={setGeneralSettings}
|
||||
passThroughItems={generalSettings}
|
||||
premiumUser={premiumUser}
|
||||
/>
|
||||
|
||||
<PassThroughEndpointsTable
|
||||
|
|
|
|||
|
|
@ -19,14 +19,9 @@ const setup = () => userEvent.setup({ pointerEventsCheck: PointerEventsCheckLeve
|
|||
|
||||
type User = ReturnType<typeof setup>;
|
||||
|
||||
const renderForm = (premiumUser = true) =>
|
||||
const renderForm = () =>
|
||||
renderWithProviders(
|
||||
<AddPassThroughEndpoint
|
||||
accessToken="test-token"
|
||||
passThroughItems={[]}
|
||||
setPassThroughItems={vi.fn()}
|
||||
premiumUser={premiumUser}
|
||||
/>,
|
||||
<AddPassThroughEndpoint accessToken="test-token" passThroughItems={[]} setPassThroughItems={vi.fn()} />,
|
||||
);
|
||||
|
||||
const openModal = async (user: User) => {
|
||||
|
|
@ -82,7 +77,7 @@ const reopenedPayload = {
|
|||
include_subpath: true,
|
||||
methods: undefined,
|
||||
default_query_params: undefined,
|
||||
auth: undefined,
|
||||
auth: true,
|
||||
timeout: undefined,
|
||||
cost_per_request: undefined,
|
||||
};
|
||||
|
|
@ -93,7 +88,7 @@ describe("add_pass_through submit payload", () => {
|
|||
createPassThroughEndpoint.mockResolvedValue({ endpoints: [{ id: "generated-id", path: "/bria" }] });
|
||||
});
|
||||
|
||||
it("sends exactly the antd payload, once, for a minimal endpoint", async () => {
|
||||
it("sends the configured headers and numeric fields once", async () => {
|
||||
const user = setup();
|
||||
renderForm();
|
||||
await openModal(user);
|
||||
|
|
@ -114,7 +109,7 @@ describe("add_pass_through submit payload", () => {
|
|||
include_subpath: true,
|
||||
methods: undefined,
|
||||
default_query_params: undefined,
|
||||
auth: undefined,
|
||||
auth: true,
|
||||
timeout: "900",
|
||||
cost_per_request: "1.5",
|
||||
});
|
||||
|
|
@ -161,32 +156,37 @@ describe("add_pass_through submit payload", () => {
|
|||
expect(lastPayload().methods).toStrictEqual(["POST"]);
|
||||
});
|
||||
|
||||
it("sends auth true once the premium security toggle is switched on", async () => {
|
||||
it("creates an authenticated endpoint without adding headers", async () => {
|
||||
const user = setup();
|
||||
renderForm(true);
|
||||
renderForm();
|
||||
await openModal(user);
|
||||
await fillRequiredFields(user);
|
||||
|
||||
const switches = screen.getAllByRole("switch");
|
||||
expect(switches).toHaveLength(2);
|
||||
await user.click(switches[1]);
|
||||
fireEvent.change(screen.getByPlaceholderText("bria"), { target: { value: "example" } });
|
||||
fireEvent.change(screen.getByPlaceholderText("https://engine.prod.bria-api.com"), {
|
||||
target: { value: "https://example.com" },
|
||||
});
|
||||
|
||||
expect(screen.getByRole("switch", { name: "Require LiteLLM authentication" })).toBeChecked();
|
||||
await submit(user);
|
||||
|
||||
await waitFor(() => expect(createPassThroughEndpoint).toHaveBeenCalled());
|
||||
expect(lastPayload().auth).toBe(true);
|
||||
await waitFor(() => expect(createPassThroughEndpoint).toHaveBeenCalledOnce());
|
||||
expect(lastPayload()).toMatchObject({ path: "/example", headers: {}, auth: true });
|
||||
});
|
||||
|
||||
it("omits the auth key entirely for a non-premium user", async () => {
|
||||
it("saves authentication off with all headers removed and resets it on reopening", async () => {
|
||||
const user = setup();
|
||||
renderForm(false);
|
||||
renderForm();
|
||||
await openModal(user);
|
||||
await fillRequiredFields(user);
|
||||
await user.click(screen.getByRole("button", { name: "Remove header 1" }));
|
||||
await user.click(screen.getByRole("switch", { name: "Require LiteLLM authentication" }));
|
||||
|
||||
await submit(user);
|
||||
|
||||
await waitFor(() => expect(createPassThroughEndpoint).toHaveBeenCalled());
|
||||
expect(lastPayload()).not.toHaveProperty("auth");
|
||||
await waitFor(() => expect(createPassThroughEndpoint).toHaveBeenCalledOnce());
|
||||
expect(lastPayload()).toMatchObject({ headers: {}, auth: false });
|
||||
await openModal(user);
|
||||
expect(screen.getByRole("switch", { name: "Require LiteLLM authentication" })).toBeChecked();
|
||||
expect(screen.queryByPlaceholderText("Header Name")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("drops include_subpath to false when the toggle is switched off", async () => {
|
||||
|
|
@ -212,7 +212,6 @@ describe("add_pass_through submit payload", () => {
|
|||
|
||||
expect(await screen.findByText("Path is required")).toBeInTheDocument();
|
||||
expect(screen.getByText("Target URL is required")).toBeInTheDocument();
|
||||
expect(screen.getByText("Please configure the headers")).toBeInTheDocument();
|
||||
expect(createPassThroughEndpoint).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -41,11 +41,9 @@ const passThroughFormSchema = z.object({
|
|||
.pipe(z.url({ error: "Please enter a valid URL" })),
|
||||
methods: z.array(z.string()).optional(),
|
||||
include_subpath: z.boolean(),
|
||||
headers: keyValuePairsSchema.refine((pairs) => pairs.some(([name]) => name !== ""), {
|
||||
error: "Please configure the headers",
|
||||
}),
|
||||
headers: keyValuePairsSchema,
|
||||
default_query_params: keyValuePairsSchema.optional(),
|
||||
auth: z.boolean().optional(),
|
||||
auth: z.boolean(),
|
||||
timeout: z.string().optional(),
|
||||
cost_per_request: z.string().optional(),
|
||||
});
|
||||
|
|
@ -59,7 +57,7 @@ const emptyFormValues = {
|
|||
include_subpath: true,
|
||||
headers: [],
|
||||
default_query_params: undefined,
|
||||
auth: undefined,
|
||||
auth: true,
|
||||
timeout: undefined,
|
||||
cost_per_request: undefined,
|
||||
} as unknown as PassThroughFormValues;
|
||||
|
|
@ -88,14 +86,12 @@ interface AddFallbacksProps {
|
|||
accessToken: string;
|
||||
passThroughItems: passThroughItem[];
|
||||
setPassThroughItems: React.Dispatch<React.SetStateAction<passThroughItem[]>>;
|
||||
premiumUser?: boolean;
|
||||
}
|
||||
|
||||
const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
||||
accessToken,
|
||||
setPassThroughItems,
|
||||
passThroughItems,
|
||||
premiumUser = false,
|
||||
}) => {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
|
@ -123,7 +119,7 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
include_subpath: values.include_subpath,
|
||||
headers: toRecord(values.headers),
|
||||
default_query_params: optionalRecord(values.default_query_params),
|
||||
...(premiumUser ? { auth: values.auth } : {}),
|
||||
auth: values.auth,
|
||||
timeout: values.timeout,
|
||||
cost_per_request: values.cost_per_request,
|
||||
...(Object.keys(guardrails).length > 0 ? { guardrails } : {}),
|
||||
|
|
@ -268,14 +264,12 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
control={form.control}
|
||||
name="headers"
|
||||
label={labelWithHint(
|
||||
"Authentication Headers",
|
||||
"Headers (Optional)",
|
||||
"Authentication and other headers to forward with requests",
|
||||
)}
|
||||
description={
|
||||
<>
|
||||
<span className="mb-1 block font-medium">
|
||||
Add authentication tokens and other required headers
|
||||
</span>
|
||||
<span className="mb-1 block font-medium">Add headers only if required by the target API</span>
|
||||
<span className="block">Common examples: auth_token, Authorization, x-api-key</span>
|
||||
</>
|
||||
}
|
||||
|
|
@ -313,13 +307,7 @@ const AddPassThroughEndpoint: React.FC<AddFallbacksProps> = ({
|
|||
</Card>
|
||||
|
||||
<FormField control={form.control} name="auth">
|
||||
{({ value, onChange }) => (
|
||||
<PassThroughSecuritySection
|
||||
premiumUser={premiumUser}
|
||||
authEnabled={value ?? false}
|
||||
onAuthChange={onChange}
|
||||
/>
|
||||
)}
|
||||
{({ value, onChange }) => <PassThroughSecuritySection authEnabled={value} onAuthChange={onChange} />}
|
||||
</FormField>
|
||||
|
||||
<PassThroughGuardrailsSection accessToken={accessToken} value={guardrails} onChange={setGuardrails} />
|
||||
|
|
|
|||
|
|
@ -4,41 +4,18 @@ import { Card } from "@/components/ui/card";
|
|||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
export interface PassThroughSecuritySectionProps {
|
||||
premiumUser: boolean;
|
||||
authEnabled: boolean;
|
||||
onAuthChange: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
const PassThroughSecuritySection: React.FC<PassThroughSecuritySectionProps> = ({
|
||||
premiumUser,
|
||||
authEnabled,
|
||||
onAuthChange,
|
||||
}) => {
|
||||
const PassThroughSecuritySection: React.FC<PassThroughSecuritySectionProps> = ({ authEnabled, onAuthChange }) => {
|
||||
return (
|
||||
<Card className="block p-6">
|
||||
<h3 className="mb-2 text-lg font-semibold text-foreground">Security</h3>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
When enabled, requests to this endpoint will require a valid LiteLLM Virtual Key
|
||||
</p>
|
||||
{premiumUser ? (
|
||||
<Switch checked={authEnabled} onCheckedChange={onAuthChange} />
|
||||
) : (
|
||||
<div>
|
||||
<div className="mb-3 flex items-center">
|
||||
<Switch disabled checked={false} />
|
||||
<span className="ml-2 text-sm text-muted-foreground">Authentication (Premium)</span>
|
||||
</div>
|
||||
<div className="rounded-lg border border-warning/20 bg-warning/10 p-3">
|
||||
<p className="text-sm text-warning">
|
||||
Setting authentication for pass-through endpoints is a LiteLLM Enterprise feature. Get a trial key{" "}
|
||||
<a href="https://www.litellm.ai/#pricing" target="_blank" rel="noopener noreferrer" className="underline">
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Switch aria-label="Require LiteLLM authentication" checked={authEnabled} onCheckedChange={onAuthChange} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,16 +31,8 @@ const endpoint = {
|
|||
methods: ["GET"],
|
||||
};
|
||||
|
||||
const renderView = (premiumUser = true, data = endpoint) =>
|
||||
renderWithProviders(
|
||||
<PassThroughInfoView
|
||||
endpointData={data}
|
||||
onClose={vi.fn()}
|
||||
accessToken="test-token"
|
||||
isAdmin
|
||||
premiumUser={premiumUser}
|
||||
/>,
|
||||
);
|
||||
const renderView = (data = endpoint) =>
|
||||
renderWithProviders(<PassThroughInfoView endpointData={data} onClose={vi.fn()} accessToken="test-token" isAdmin />);
|
||||
|
||||
const openEditForm = async (user: User) => {
|
||||
await user.click(screen.getByRole("tab", { name: "Settings" }));
|
||||
|
|
@ -122,20 +114,34 @@ describe("pass_through_info update payload", () => {
|
|||
expect(lastPayload().headers).toStrictEqual({ "x-api-key": "k" });
|
||||
});
|
||||
|
||||
it("sends auth undefined for a non-premium user", async () => {
|
||||
it.each([true, false])("loads auth %s and persists the opposite choice", async (auth) => {
|
||||
const user = setup();
|
||||
renderView(false);
|
||||
renderView({ ...endpoint, headers: {}, auth });
|
||||
await openEditForm(user);
|
||||
|
||||
const toggle = screen.getByRole("switch", { name: "Require LiteLLM authentication" });
|
||||
if (auth) {
|
||||
expect(toggle).toBeChecked();
|
||||
} else {
|
||||
expect(toggle).not.toBeChecked();
|
||||
}
|
||||
await user.click(toggle);
|
||||
await save(user);
|
||||
|
||||
await waitFor(() => expect(updatePassThroughEndpoint).toHaveBeenCalled());
|
||||
expect(lastPayload().auth).toBeUndefined();
|
||||
await waitFor(() => expect(updatePassThroughEndpoint).toHaveBeenCalledOnce());
|
||||
expect(lastPayload()).toMatchObject({ headers: {}, auth: !auth });
|
||||
await openEditForm(user);
|
||||
const savedToggle = screen.getByRole("switch", { name: "Require LiteLLM authentication" });
|
||||
if (auth) {
|
||||
expect(savedToggle).not.toBeChecked();
|
||||
} else {
|
||||
expect(savedToggle).toBeChecked();
|
||||
}
|
||||
});
|
||||
|
||||
it("sends methods undefined when the endpoint has none", async () => {
|
||||
const user = setup();
|
||||
renderView(true, { ...endpoint, methods: [] });
|
||||
renderView({ ...endpoint, methods: [] });
|
||||
await openEditForm(user);
|
||||
|
||||
await save(user);
|
||||
|
|
|
|||
|
|
@ -91,7 +91,6 @@ export interface PassThroughInfoProps {
|
|||
onClose: () => void;
|
||||
accessToken: string | null;
|
||||
isAdmin: boolean;
|
||||
premiumUser?: boolean;
|
||||
onEndpointUpdated?: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +138,6 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
|
|||
onClose,
|
||||
accessToken,
|
||||
isAdmin,
|
||||
premiumUser = false,
|
||||
onEndpointUpdated,
|
||||
}) => {
|
||||
const [endpointData, setEndpointData] = useState<PassThroughEndpoint | null>(initialEndpointData);
|
||||
|
|
@ -157,7 +155,7 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
|
|||
include_subpath: initialEndpointData.include_subpath || false,
|
||||
cost_per_request: initialEndpointData.cost_per_request,
|
||||
timeout: initialEndpointData.timeout,
|
||||
auth: initialEndpointData.auth || false,
|
||||
auth: initialEndpointData.auth ?? true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -189,7 +187,7 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
|
|||
include_subpath: values.include_subpath,
|
||||
cost_per_request: values.cost_per_request,
|
||||
timeout: values.timeout,
|
||||
auth: premiumUser ? values.auth : undefined,
|
||||
auth: values.auth,
|
||||
methods: values.methods.length > 0 ? values.methods : undefined,
|
||||
guardrails: guardrails && Object.keys(guardrails).length > 0 ? guardrails : undefined,
|
||||
};
|
||||
|
|
@ -472,11 +470,7 @@ const PassThroughInfoView: React.FC<PassThroughInfoProps> = ({
|
|||
|
||||
<FormField control={form.control} name="auth">
|
||||
{({ value, onChange }) => (
|
||||
<PassThroughSecuritySection
|
||||
premiumUser={premiumUser}
|
||||
authEnabled={value}
|
||||
onAuthChange={onChange}
|
||||
/>
|
||||
<PassThroughSecuritySection authEnabled={value} onAuthChange={onChange} />
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue