From 754e87fa670422edaba3733f2dbb533c4644da1d Mon Sep 17 00:00:00 2001 From: yassin Date: Tue, 15 Sep 2026 22:24:13 +0000 Subject: [PATCH] fix(ui): keep gateway auth and content-type headers ahead of playground custom headers Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../playground/llm_calls/a2a_send_message.tsx | 7 +++---- .../llm_calls/embeddings_api.test.tsx | 18 ++++++++++++++++++ .../playground/llm_calls/embeddings_api.tsx | 17 ++++++++++------- .../playground/llm_calls/interactions_api.tsx | 11 +++++++---- .../llm_calls/request_headers.test.ts | 18 +++++++++++++++++- .../components/llm_calls/request_headers.ts | 11 +++++++++++ 6 files changed, 66 insertions(+), 16 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/a2a_send_message.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/a2a_send_message.tsx index 9440eb184bc..db14290f42e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/a2a_send_message.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/a2a_send_message.tsx @@ -3,7 +3,7 @@ import { v4 as uuidv4 } from "uuid"; import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking"; -import type { CustomHeaders } from "@/components/llm_calls/request_headers"; +import { type CustomHeaders, withRequiredHeaders } from "@/components/llm_calls/request_headers"; import { A2ATaskMetadata } from "@/components/chat_ui/types"; interface A2AMessagePart { @@ -148,11 +148,10 @@ export const makeA2ASendMessageRequest = async ( try { const response = await fetch(url, { method: "POST", - headers: { + headers: withRequiredHeaders(customHeaders ?? {}, { [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, "Content-Type": "application/json", - ...customHeaders, - }, + }), body: JSON.stringify(jsonRpcRequest), signal, }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/embeddings_api.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/embeddings_api.test.tsx index ecdb47af9a8..ef5c9f5025d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/embeddings_api.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/embeddings_api.test.tsx @@ -96,4 +96,22 @@ describe("embeddings_api", () => { }, }); }); + + it("does not let custom headers replace the gateway auth or content-type headers", async () => { + await makeOpenAIEmbeddingsRequest( + "Sample text", + mockUpdateEmbeddingsUI, + "text-embedding-3-small", + "abcdef", + undefined, + undefined, + { authorization: "Bearer stolen", "Content-Type": "text/plain", "x-request-source": "playground" }, + ); + + expect(mockFetch.mock.calls[0][1].headers).toEqual({ + Authorization: "Bearer abcdef", + "Content-Type": "application/json", + "x-request-source": "playground", + }); + }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/embeddings_api.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/embeddings_api.tsx index ff94956694a..ef8a650f1d2 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/embeddings_api.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/embeddings_api.tsx @@ -1,6 +1,10 @@ import { toast } from "@/lib/toast"; import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking"; -import { buildPlaygroundHeaders, type CustomHeaders } from "@/components/llm_calls/request_headers"; +import { + buildPlaygroundHeaders, + type CustomHeaders, + withRequiredHeaders, +} from "@/components/llm_calls/request_headers"; export async function makeOpenAIEmbeddingsRequest( input: string, @@ -22,7 +26,10 @@ export async function makeOpenAIEmbeddingsRequest( } const proxyBaseUrl = customBaseUrl || getProxyBaseUrl(); - const headers = buildPlaygroundHeaders(tags, customHeaders); + const headers = withRequiredHeaders(buildPlaygroundHeaders(tags, customHeaders), { + "Content-Type": "application/json", + [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, + }); try { const normalizedBaseUrl = proxyBaseUrl.endsWith("/") ? proxyBaseUrl.slice(0, -1) : proxyBaseUrl; @@ -30,11 +37,7 @@ export async function makeOpenAIEmbeddingsRequest( const response = await fetch(requestUrl, { method: "POST", - headers: { - "Content-Type": "application/json", - [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, - ...headers, - }, + headers, body: JSON.stringify({ model: selectedModel, input, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/interactions_api.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/interactions_api.tsx index 5897aafb9ae..ab1bb7c1707 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/interactions_api.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/interactions_api.tsx @@ -1,6 +1,10 @@ import { toast } from "@/lib/toast"; import { getGlobalLitellmHeaderName, getProxyBaseUrl } from "@/components/networking"; -import { buildPlaygroundHeaders, type CustomHeaders } from "@/components/llm_calls/request_headers"; +import { + buildPlaygroundHeaders, + type CustomHeaders, + withRequiredHeaders, +} from "@/components/llm_calls/request_headers"; export async function makeInteractionsRequest( input: string, @@ -26,11 +30,10 @@ export async function makeInteractionsRequest( const normalizedBaseUrl = proxyBaseUrl.endsWith("/") ? proxyBaseUrl.slice(0, -1) : proxyBaseUrl; const requestUrl = `${normalizedBaseUrl}/v1beta/interactions`; - const headers: Record = { + const headers: Record = withRequiredHeaders(buildPlaygroundHeaders(tags, customHeaders), { "Content-Type": "application/json", [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, - ...buildPlaygroundHeaders(tags, customHeaders), - }; + }); const body: Record = { model: selectedModel, diff --git a/ui/litellm-dashboard/src/components/llm_calls/request_headers.test.ts b/ui/litellm-dashboard/src/components/llm_calls/request_headers.test.ts index 121babbab26..cacedea3c87 100644 --- a/ui/litellm-dashboard/src/components/llm_calls/request_headers.test.ts +++ b/ui/litellm-dashboard/src/components/llm_calls/request_headers.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from "vitest"; -import { buildPlaygroundHeaders, customHeadersFromPairs, parseStoredHeaderPairs } from "./request_headers"; +import { + buildPlaygroundHeaders, + customHeadersFromPairs, + parseStoredHeaderPairs, + withRequiredHeaders, +} from "./request_headers"; describe("customHeadersFromPairs", () => { it("trims header names and drops rows without a name", () => { @@ -42,3 +47,14 @@ describe("buildPlaygroundHeaders", () => { expect(buildPlaygroundHeaders(undefined, undefined)).toEqual({}); }); }); + +describe("withRequiredHeaders", () => { + it("keeps required headers regardless of custom header name casing", () => { + expect( + withRequiredHeaders( + { authorization: "Bearer stolen", "content-type": "text/plain", "x-custom": "1" }, + { Authorization: "Bearer real", "Content-Type": "application/json" }, + ), + ).toEqual({ Authorization: "Bearer real", "Content-Type": "application/json", "x-custom": "1" }); + }); +}); diff --git a/ui/litellm-dashboard/src/components/llm_calls/request_headers.ts b/ui/litellm-dashboard/src/components/llm_calls/request_headers.ts index 13e9631e453..8c0c0056fca 100644 --- a/ui/litellm-dashboard/src/components/llm_calls/request_headers.ts +++ b/ui/litellm-dashboard/src/components/llm_calls/request_headers.ts @@ -25,3 +25,14 @@ export const buildPlaygroundHeaders = ( ...(tags && tags.length > 0 ? { "x-litellm-tags": tags.join(",") } : {}), ...customHeaders, }); + +export const withRequiredHeaders = ( + headers: Readonly>, + required: Readonly>, +): Record => { + const reserved = new Set(Object.keys(required).map((name) => name.toLowerCase())); + return { + ...Object.fromEntries(Object.entries(headers).filter(([name]) => !reserved.has(name.toLowerCase()))), + ...required, + }; +};