mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix(ui): respect litellm_key_header_name in BYOK credential save and workflow runs fetches (#33103)
This commit is contained in:
parent
c75fccfd63
commit
7fce761cde
9 changed files with 324 additions and 180 deletions
|
|
@ -515,6 +515,152 @@
|
|||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/MCPLogoSelector.test.tsx": {
|
||||
"unused-imports/no-unused-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/MCPNetworkSettings.tsx": {
|
||||
"react-hooks/immutability": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/MCPSubmissionsTab.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
},
|
||||
"unused-imports/no-unused-imports": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/OAuthFormFields.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/OpenAPIQuickPicker.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/ToolTestPanel.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 3
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/UserEnvVarsModal.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/create_mcp_server.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/static-components": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_connection_status.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 3
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_discovery.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_config.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_display.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/immutability": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_server_view.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_tool_configuration.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_tools.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/memory/_components/MemoryView.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
|
|
@ -1860,45 +2006,11 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/mcp_tools/ByokCredentialModal.tsx": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/MCPLogoSelector.test.tsx": {
|
||||
"unused-imports/no-unused-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/MCPNetworkSettings.tsx": {
|
||||
"react-hooks/immutability": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/MCPSubmissionsTab.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/mcp_tools/MCPToolArgumentsForm.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
},
|
||||
"unused-imports/no-unused-imports": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/components/mcp_tools/McpCrudPermissionPanel.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 3
|
||||
|
|
@ -1907,123 +2019,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/OAuthFormFields.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/OpenAPIQuickPicker.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/ToolTestPanel.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 3
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/UserEnvVarsModal.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/create_mcp_server.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/static-components": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_connection_status.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 3
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_discovery.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_config.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_display.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/immutability": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_server_view.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_tool_configuration.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/app/(dashboard)/mcp-servers/_components/mcp_tools.tsx": {
|
||||
"no-nested-ternary": {
|
||||
"count": 1
|
||||
},
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"src/components/model_add/AddCredentialModal.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
|
@ -2545,4 +2540,4 @@
|
|||
"count": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -684,7 +684,6 @@ const MCPServers: React.FC<MCPServerProps> = ({ accessToken, userRole, userID })
|
|||
refetch();
|
||||
setByokModalServer(null);
|
||||
}}
|
||||
accessToken={accessToken || ""}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -2186,7 +2186,6 @@ const ChatUI: React.FC<ChatUIProps> = ({
|
|||
loadMCPServers();
|
||||
setByokModalServer(null);
|
||||
}}
|
||||
accessToken={accessToken || ""}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|||
|
||||
import WorkflowRuns from "./WorkflowRuns";
|
||||
|
||||
vi.mock("@/components/networking", () => ({ proxyBaseUrl: "" }));
|
||||
vi.mock("@/components/networking", () => ({
|
||||
proxyBaseUrl: "",
|
||||
getGlobalLitellmHeaderName: () => "x-litellm-api-key",
|
||||
}));
|
||||
|
||||
interface FakeRun {
|
||||
run_id: string;
|
||||
|
|
@ -78,4 +81,18 @@ describe("WorkflowRuns (migrated onto shared DataTable)", () => {
|
|||
|
||||
expect(await screen.findByText("No workflow runs yet")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sends the configured litellm key header on every fetch instead of hardcoding Authorization", async () => {
|
||||
const user = userEvent.setup();
|
||||
const fetchSpy = mockFetch(RUNS);
|
||||
vi.stubGlobal("fetch", fetchSpy);
|
||||
render(<WorkflowRuns accessToken="tok" />);
|
||||
|
||||
await user.click(await screen.findByText("First run"));
|
||||
|
||||
await waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(3));
|
||||
for (const [url, init] of fetchSpy.mock.calls as [string, RequestInit][]) {
|
||||
expect(init.headers, url).toEqual({ "x-litellm-api-key": "Bearer tok" });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|||
import { Button, Collapse, Drawer, Empty, Spin, Tooltip, Typography } from "antd";
|
||||
import { ReloadOutlined } from "@ant-design/icons";
|
||||
import type { ColumnDef, ColumnFiltersState } from "@tanstack/react-table";
|
||||
import { proxyBaseUrl } from "@/components/networking";
|
||||
import { getGlobalLitellmHeaderName, proxyBaseUrl } from "@/components/networking";
|
||||
import {
|
||||
DataTable,
|
||||
DataTableFilterDrawer,
|
||||
|
|
@ -507,7 +507,7 @@ const WorkflowRuns: React.FC<WorkflowRunsProps> = ({ accessToken }) => {
|
|||
setLoadingRuns(true);
|
||||
try {
|
||||
const res = await fetch(`${proxyBaseUrl ?? ""}/v1/workflows/runs?limit=100`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
headers: { [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}` },
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
|
|
@ -531,10 +531,10 @@ const WorkflowRuns: React.FC<WorkflowRunsProps> = ({ accessToken }) => {
|
|||
const base = proxyBaseUrl ?? "";
|
||||
const [evRes, msgRes] = await Promise.all([
|
||||
fetch(`${base}/v1/workflows/runs/${run.run_id}/events`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
headers: { [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}` },
|
||||
}),
|
||||
fetch(`${base}/v1/workflows/runs/${run.run_id}/messages`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
headers: { [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}` },
|
||||
}),
|
||||
]);
|
||||
const evData = evRes.ok ? await evRes.json() : { events: [] };
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { registerAuthHeaderNameGetter, registerAuthTokenGetter, registerBaseUrlGetter } from "@/lib/http/runtime";
|
||||
import { ByokCredentialModal } from "./ByokCredentialModal";
|
||||
import type { MCPServer } from "./types";
|
||||
|
||||
const fetchSpy = vi.hoisted(() => {
|
||||
const spy = vi.fn<(request: Request) => Promise<Response>>();
|
||||
vi.stubGlobal("fetch", spy);
|
||||
return spy;
|
||||
});
|
||||
|
||||
vi.mock("@/components/molecules/message_manager", () => ({
|
||||
default: { success: vi.fn(), error: vi.fn() },
|
||||
}));
|
||||
|
||||
const SERVER = { server_id: "srv-1", alias: "Linear", server_name: "Linear" } as MCPServer;
|
||||
|
||||
const jsonResponse = (body: unknown, status = 200) =>
|
||||
new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
|
||||
|
||||
async function fillAndSubmit(user: ReturnType<typeof userEvent.setup>) {
|
||||
await user.click(screen.getByText("Continue to Authentication"));
|
||||
await user.type(screen.getByPlaceholderText("Enter your API key"), "linear-key");
|
||||
await user.click(screen.getByRole("button", { name: /Connect & Authorize/ }));
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fetchSpy.mockReset();
|
||||
registerBaseUrlGetter(() => "");
|
||||
registerAuthTokenGetter(() => "sk-session");
|
||||
});
|
||||
|
||||
describe("ByokCredentialModal", () => {
|
||||
it("saves the credential with the session's configured litellm key header, not a hardcoded Authorization", async () => {
|
||||
registerAuthHeaderNameGetter(() => "x-litellm-api-key");
|
||||
fetchSpy.mockResolvedValue(jsonResponse({ server_id: "srv-1", has_credential: true }));
|
||||
const onSuccess = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
render(<ByokCredentialModal server={SERVER} open onClose={() => {}} onSuccess={onSuccess} />);
|
||||
|
||||
await fillAndSubmit(user);
|
||||
|
||||
await waitFor(() => expect(onSuccess).toHaveBeenCalledWith("srv-1"));
|
||||
const request = fetchSpy.mock.calls[0][0];
|
||||
expect(request.method).toBe("POST");
|
||||
expect(new URL(request.url).pathname).toBe("/v1/mcp/server/srv-1/user-credential");
|
||||
expect(request.headers.get("x-litellm-api-key")).toBe("Bearer sk-session");
|
||||
expect(request.headers.get("Authorization")).toBeNull();
|
||||
expect(await request.json()).toEqual({ credential: "linear-key", save: true });
|
||||
});
|
||||
|
||||
it("surfaces the backend's detail.error message when the save fails", async () => {
|
||||
registerAuthHeaderNameGetter(() => "Authorization");
|
||||
fetchSpy.mockResolvedValue(
|
||||
jsonResponse({ detail: { error: "This MCP server does not support BYOK credentials" } }, 400),
|
||||
);
|
||||
const MessageManager = (await import("@/components/molecules/message_manager")).default;
|
||||
const onSuccess = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
render(<ByokCredentialModal server={SERVER} open onClose={() => {}} onSuccess={onSuccess} />);
|
||||
|
||||
await fillAndSubmit(user);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(MessageManager.error).toHaveBeenCalledWith("This MCP server does not support BYOK credentials"),
|
||||
);
|
||||
expect(onSuccess).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
import React, { useState } from "react";
|
||||
import { Modal, Input, Switch } from "antd";
|
||||
import MessageManager from "@/components/molecules/message_manager";
|
||||
import { fetchClient } from "@/lib/http/api";
|
||||
import { ApiError } from "@/lib/http/client";
|
||||
import {
|
||||
KeyOutlined,
|
||||
LockOutlined,
|
||||
|
|
@ -14,21 +16,22 @@ import {
|
|||
} from "@ant-design/icons";
|
||||
import { MCPServer } from "./types";
|
||||
|
||||
const byokSaveErrorMessage = (e: unknown): string => {
|
||||
if (e instanceof ApiError) {
|
||||
const detail = (e.body as { detail?: { error?: string } } | null)?.detail?.error;
|
||||
if (detail) return detail;
|
||||
}
|
||||
return e instanceof Error && e.message ? e.message : "Failed to connect";
|
||||
};
|
||||
|
||||
interface ByokCredentialModalProps {
|
||||
server: MCPServer;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: (serverId: string) => void;
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
|
||||
server,
|
||||
open,
|
||||
onClose,
|
||||
onSuccess,
|
||||
accessToken,
|
||||
}) => {
|
||||
export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({ server, open, onClose, onSuccess }) => {
|
||||
const [step, setStep] = useState<1 | 2>(1);
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [saveKey, setSaveKey] = useState(true);
|
||||
|
|
@ -52,23 +55,15 @@ export const ByokCredentialModal: React.FC<ByokCredentialModalProps> = ({
|
|||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/v1/mcp/server/${server.server_id}/user-credential`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify({ credential: apiKey.trim(), save: saveKey }),
|
||||
await fetchClient.POST("/v1/mcp/server/{server_id}/user-credential", {
|
||||
params: { path: { server_id: server.server_id } },
|
||||
body: { credential: apiKey.trim(), save: saveKey },
|
||||
});
|
||||
if (!response.ok) {
|
||||
const err = await response.json();
|
||||
throw new Error(err?.detail?.error || "Failed to save credential");
|
||||
}
|
||||
MessageManager.success(`Connected to ${serverDisplayName}`);
|
||||
onSuccess(server.server_id);
|
||||
handleClose();
|
||||
} catch (e: any) {
|
||||
MessageManager.error(e.message || "Failed to connect");
|
||||
} catch (e) {
|
||||
MessageManager.error(byokSaveErrorMessage(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -530,3 +530,67 @@ describe("buildModelGroupTestRequest", () => {
|
|||
expect(body).toEqual({ model: "text-embedding-3-small", input: "test from litellm" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("testMCPToolsListRequest auth headers", () => {
|
||||
const originalFetch = global.fetch;
|
||||
|
||||
const captureFetch = () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: { get: () => "application/json" },
|
||||
json: vi.fn().mockResolvedValue({ tools: [] }),
|
||||
} as any);
|
||||
global.fetch = mockFetch as any;
|
||||
return mockFetch;
|
||||
};
|
||||
|
||||
const sentHeaders = (mockFetch: ReturnType<typeof vi.fn>): Record<string, string> =>
|
||||
(mockFetch.mock.calls[0][1] as RequestInit).headers as Record<string, string>;
|
||||
|
||||
afterEach(() => {
|
||||
Networking.setGlobalLitellmHeaderName("Authorization");
|
||||
global.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it("sends the litellm key under a custom litellm_key_header_name even when an upstream OAuth token uses Authorization", async () => {
|
||||
Networking.setGlobalLitellmHeaderName("x-litellm-key");
|
||||
const mockFetch = captureFetch();
|
||||
|
||||
await Networking.testMCPToolsListRequest("sk-key", {}, "upstream-oauth-token");
|
||||
|
||||
const headers = sentHeaders(mockFetch);
|
||||
expect(headers["x-litellm-key"]).toBe("Bearer sk-key");
|
||||
expect(headers["Authorization"]).toBe("Bearer upstream-oauth-token");
|
||||
});
|
||||
|
||||
it("Bearer-prefixes x-litellm-api-key when it is the configured key header (raw values fail _get_bearer_token)", async () => {
|
||||
Networking.setGlobalLitellmHeaderName("x-litellm-api-key");
|
||||
const mockFetch = captureFetch();
|
||||
|
||||
await Networking.testMCPToolsListRequest("sk-key", {}, "upstream-oauth-token");
|
||||
|
||||
const headers = sentHeaders(mockFetch);
|
||||
expect(headers["x-litellm-api-key"]).toBe("Bearer sk-key");
|
||||
expect(headers["Authorization"]).toBe("Bearer upstream-oauth-token");
|
||||
});
|
||||
|
||||
it("never clobbers the upstream OAuth token on default deployments", async () => {
|
||||
const mockFetch = captureFetch();
|
||||
|
||||
await Networking.testMCPToolsListRequest("sk-key", {}, "upstream-oauth-token");
|
||||
|
||||
const headers = sentHeaders(mockFetch);
|
||||
expect(headers["Authorization"]).toBe("Bearer upstream-oauth-token");
|
||||
expect(headers["x-litellm-api-key"]).toBe("sk-key");
|
||||
});
|
||||
|
||||
it("sends the litellm key as the bearer on default deployments without an OAuth token", async () => {
|
||||
const mockFetch = captureFetch();
|
||||
|
||||
await Networking.testMCPToolsListRequest("sk-key", {});
|
||||
|
||||
const headers = sentHeaders(mockFetch);
|
||||
expect(headers["Authorization"]).toBe("Bearer sk-key");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6650,6 +6650,9 @@ export const testMCPToolsListRequest = async (
|
|||
};
|
||||
if (accessToken) {
|
||||
headers["x-litellm-api-key"] = accessToken;
|
||||
if (globalLitellmHeaderName.toLowerCase() !== "authorization") {
|
||||
headers[globalLitellmHeaderName] = `Bearer ${accessToken}`;
|
||||
}
|
||||
}
|
||||
if (oauthAccessToken) {
|
||||
headers["Authorization"] = `Bearer ${oauthAccessToken}`;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue