mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
feat(ui): move the dcr_bridge toggle next to the OAuth app fields
Render DcrBridgeToggle inside PassthroughAuthorizeSection, after the OAuth client ID/secret fields and just before the Authorize & Fetch Tools button, in both the create and edit flows. Also update the section copy to say a configured OAuth app is saved with the server, using the same wording as the credential lifecycle rework in #32752 so whichever PR lands second rebases cleanly
This commit is contained in:
parent
47f33bda3f
commit
2b4c054403
5 changed files with 50 additions and 20 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import React from "react";
|
||||
import { Button, Form, Input } from "antd";
|
||||
import DcrBridgeToggle from "./DcrBridgeToggle";
|
||||
import { isClientForwardedTokenMode } from "./types";
|
||||
|
||||
interface PassthroughOAuthFlow {
|
||||
|
|
@ -11,20 +12,23 @@ interface PassthroughOAuthFlow {
|
|||
|
||||
/**
|
||||
* Browser-only Authorize & Fetch for the client-forwarded token modes
|
||||
* (true_passthrough / oauth_delegate). LiteLLM never stores upstream
|
||||
* credentials for these modes, so the token obtained here lives in this
|
||||
* browser session only: it is forwarded per-server for the tools preview and
|
||||
* allowlist configuration, and is never written to the server row or the
|
||||
* per-user credential store. The optional client credentials cover IdPs
|
||||
* without dynamic client registration (e.g. a pre-registered Slack app) and
|
||||
* ride the temporary authorize session only.
|
||||
* (true_passthrough / oauth_delegate). Tokens are never stored: the token
|
||||
* obtained here lives in this browser session only, forwarded per-server for
|
||||
* the tools preview and allowlist configuration, and is never written to the
|
||||
* server row or the per-user credential store. The optional OAuth client
|
||||
* credentials cover IdPs without dynamic client registration (e.g. a
|
||||
* pre-registered Slack app); unlike the token they ARE saved onto the server
|
||||
* as declared config, so internal users' Authorize relays through the org's
|
||||
* app instead of dead-ending on upstreams that cannot mint clients.
|
||||
*/
|
||||
export default function PassthroughAuthorizeSection({
|
||||
authType,
|
||||
oauthFlow,
|
||||
dcrBridgeInitialChecked,
|
||||
}: {
|
||||
authType?: string | null;
|
||||
oauthFlow: PassthroughOAuthFlow;
|
||||
dcrBridgeInitialChecked?: boolean;
|
||||
}) {
|
||||
if (!isClientForwardedTokenMode(authType)) return null;
|
||||
const authorizeButtonLabels: Record<string, string> = {
|
||||
|
|
@ -35,14 +39,15 @@ export default function PassthroughAuthorizeSection({
|
|||
return (
|
||||
<div className="rounded-lg border border-dashed border-gray-300 p-4 space-y-2 mb-4">
|
||||
<p className="text-sm text-gray-600">
|
||||
Callers bring their own upstream token for this auth type, so LiteLLM stores no upstream credentials. To preview
|
||||
tools and configure the tool allowlist, authorize against the upstream here: the token stays in this browser
|
||||
session only and is never saved to LiteLLM.
|
||||
Callers bring their own upstream token for this auth type, so LiteLLM never stores tokens. To preview tools and
|
||||
configure the tool allowlist, authorize against the upstream here: the token stays in this browser session only
|
||||
and is never saved to LiteLLM. An OAuth app configured below IS saved with the server, so internal users who
|
||||
authorize from the Tools page go through it.
|
||||
</p>
|
||||
<Form.Item
|
||||
label={<span className="text-sm font-medium text-gray-700">OAuth Client ID (optional, not saved)</span>}
|
||||
label={<span className="text-sm font-medium text-gray-700">OAuth Client ID (optional, saved)</span>}
|
||||
name={["credentials", "client_id"]}
|
||||
extra="Only needed when the upstream does not support dynamic client registration (e.g. a pre-registered Slack app). Used for this browser authorization only."
|
||||
extra="Set this to make everyone authorize through a specific app; required for upstreams without dynamic client registration (e.g. a pre-registered Slack app)."
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="Leave blank to use dynamic client registration"
|
||||
|
|
@ -50,7 +55,7 @@ export default function PassthroughAuthorizeSection({
|
|||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={<span className="text-sm font-medium text-gray-700">OAuth Client Secret (optional, not saved)</span>}
|
||||
label={<span className="text-sm font-medium text-gray-700">OAuth Client Secret (optional, saved)</span>}
|
||||
name={["credentials", "client_secret"]}
|
||||
>
|
||||
<Input.Password
|
||||
|
|
@ -58,6 +63,7 @@ export default function PassthroughAuthorizeSection({
|
|||
className="rounded-lg border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
||||
/>
|
||||
</Form.Item>
|
||||
<DcrBridgeToggle authType={authType} initialChecked={dcrBridgeInitialChecked} />
|
||||
<Button
|
||||
onClick={oauthFlow.startOAuthFlow}
|
||||
disabled={oauthFlow.status === "authorizing" || oauthFlow.status === "exchanging"}
|
||||
|
|
|
|||
|
|
@ -202,8 +202,8 @@ describe("CreateMCPServer", () => {
|
|||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Authorize & Fetch Tools (browser-only)" })).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("OAuth Client ID (optional, not saved)")).toBeInTheDocument();
|
||||
expect(screen.getByText("OAuth Client Secret (optional, not saved)")).toBeInTheDocument();
|
||||
expect(screen.getByText("OAuth Client ID (optional, saved)")).toBeInTheDocument();
|
||||
expect(screen.getByText("OAuth Client Secret (optional, saved)")).toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -1586,6 +1586,21 @@ describe("CreateMCPServer dcr_bridge toggle", () => {
|
|||
expect(getDcrToggle()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the toggle between the OAuth client fields and the Authorize button", async () => {
|
||||
await setupHttpServerForm();
|
||||
|
||||
await selectAntOption("Authentication", "True Passthrough (no LiteLLM auth)");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getDcrToggle()).toBeInTheDocument();
|
||||
});
|
||||
const toggle = getDcrToggle() as HTMLElement;
|
||||
const secretInput = screen.getByPlaceholderText("Leave blank for public clients / PKCE");
|
||||
const authorizeButton = screen.getByRole("button", { name: "Authorize & Fetch Tools (browser-only)" });
|
||||
expect(secretInput.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(toggle.compareDocumentPosition(authorizeButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["true_passthrough", "True Passthrough (no LiteLLM auth)"],
|
||||
["oauth_delegate", "OAuth Delegate (client-supplied upstream token)"],
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import {
|
|||
} from "./types";
|
||||
import OAuthFormFields from "./OAuthFormFields";
|
||||
import TruePassthroughWarning from "./TruePassthroughWarning";
|
||||
import DcrBridgeToggle from "./DcrBridgeToggle";
|
||||
import PassthroughAuthorizeSection from "./PassthroughAuthorizeSection";
|
||||
import TokenExchangeFormFields from "./TokenExchangeFormFields";
|
||||
import MCPServerCostConfig from "./mcp_server_cost_config";
|
||||
|
|
@ -994,10 +993,9 @@ const CreateMCPServer: React.FC<CreateMCPServerProps> = ({
|
|||
|
||||
<TruePassthroughWarning authType={authType} />
|
||||
|
||||
<DcrBridgeToggle authType={authType} initialChecked />
|
||||
|
||||
<PassthroughAuthorizeSection
|
||||
authType={authType}
|
||||
dcrBridgeInitialChecked
|
||||
oauthFlow={{
|
||||
startOAuthFlow,
|
||||
status: oauthStatus,
|
||||
|
|
|
|||
|
|
@ -1789,6 +1789,19 @@ describe("MCPServerEdit (dcr_bridge toggle)", () => {
|
|||
expect(getDcrToggle()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders the toggle between the OAuth client fields and the Authorize button", async () => {
|
||||
renderEdit({ auth_type: "true_passthrough" });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getDcrToggle()).toBeInTheDocument();
|
||||
});
|
||||
const toggle = getDcrToggle() as HTMLElement;
|
||||
const secretInput = screen.getByPlaceholderText("Leave blank for public clients / PKCE");
|
||||
const authorizeButton = screen.getByRole("button", { name: "Authorize & Fetch Tools (browser-only)" });
|
||||
expect(secretInput.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(toggle.compareDocumentPosition(authorizeButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
});
|
||||
|
||||
it("initializes unchecked from a null stored value and saves an explicit false", async () => {
|
||||
vi.mocked(networking.updateMCPServer).mockResolvedValue({
|
||||
...interactiveOAuthServer,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import { buildMcpPassthroughAuthHeader } from "@/utils/mcpHeaderUtils";
|
|||
import MCPServerCostConfig from "./mcp_server_cost_config";
|
||||
import MCPPermissionManagement from "./MCPPermissionManagement";
|
||||
import TruePassthroughWarning from "./TruePassthroughWarning";
|
||||
import DcrBridgeToggle from "./DcrBridgeToggle";
|
||||
import PassthroughAuthorizeSection from "./PassthroughAuthorizeSection";
|
||||
import MCPToolConfiguration from "./mcp_tool_configuration";
|
||||
import StdioConfiguration from "./StdioConfiguration";
|
||||
|
|
@ -1044,7 +1043,6 @@ const MCPServerEdit: React.FC<MCPServerEditProps> = ({
|
|||
</Select>
|
||||
</Form.Item>
|
||||
<TruePassthroughWarning authType={authType} />
|
||||
<DcrBridgeToggle authType={authType} />
|
||||
<PassthroughAuthorizeSection
|
||||
authType={authType}
|
||||
oauthFlow={{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue