From ca4e7421158f3721dc732337aea6a02d5ef31608 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Fri, 16 Oct 2026 10:55:55 -0700 Subject: [PATCH] fix(ui): write playground key settings only through the encoded storage path Key source, virtual key and custom headers now go through their own write loop, separate from the plain settings, so a key value can never take the plain sessionStorage branch --- .../chat/PlaygroundChat.integration.test.tsx | 15 +++++++- .../chat/playgroundSettings.test.ts | 9 ++--- .../components/chat/playgroundSettings.ts | 35 ++++++++++--------- .../components/chat/usePlaygroundSettings.ts | 19 ++++++++-- 4 files changed, 54 insertions(+), 24 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat/PlaygroundChat.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat/PlaygroundChat.integration.test.tsx index bd3ac28f582..f712f5cb65d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat/PlaygroundChat.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat/PlaygroundChat.integration.test.tsx @@ -6,7 +6,7 @@ import { makeOpenAIChatCompletionRequest } from "@/components/llm_calls/chat_com import { fetchAvailableModels } from "@/components/llm_calls/fetch_models"; import { makeOpenAIResponsesRequest } from "@/components/llm_calls/responses_api"; import { callMCPTool, fetchMCPServers, fetchMCPToolsets, listMCPTools } from "@/components/networking"; -import { setSecureItem } from "@/utils/secureStorage"; +import { getSecureItem, setSecureItem } from "@/utils/secureStorage"; import { PlaygroundChat } from "./PlaygroundChat"; vi.mock("@/app/(dashboard)/hooks/useCan", () => ({ default: () => true })); @@ -487,4 +487,17 @@ describe("PlaygroundChat", () => { expect(screen.queryByText("Python Code Executed")).not.toBeInTheDocument(); }); + + it("keeps the virtual key encoded in session storage", async () => { + setSecureItem("apiKeySource", JSON.stringify("custom")); + setSecureItem("apiKey", "sk-virtual-123"); + preload({ selectedModel: "gpt-fast" }); + renderChat(); + + await waitFor(() => expect(fetchAvailableModels).toHaveBeenCalledWith("sk-virtual-123")); + expect(getSecureItem("apiKey")).toBe("sk-virtual-123"); + const stored = Object.keys(sessionStorage).map((key) => sessionStorage.getItem(key) ?? ""); + expect(stored.length).toBeGreaterThan(0); + expect(stored.filter((value) => value.includes("sk-virtual-123"))).toEqual([]); + }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat/playgroundSettings.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat/playgroundSettings.test.ts index 1b4e4daa173..87e97bde2cf 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat/playgroundSettings.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat/playgroundSettings.test.ts @@ -6,7 +6,8 @@ import { allowedTools, defaultSettings, loadSettings, - storedValues, + plainValues, + secureValues, toggleMcpItem, toggleToolLimit, withEndpoint, @@ -29,7 +30,7 @@ const TOOLS = ["search", "fetch", "summarize"] as const; const SCOPE = { server: "s", tools: TOOLS }; describe("loadSettings", () => { - it("restores every persisted field from what storedValues wrote", () => { + it("restores every persisted field from what was written", () => { const persisted: Partial = { keySource: "custom", virtualKey: "sk-test", @@ -48,8 +49,8 @@ describe("loadSettings", () => { }; const original = settingsWith(persisted); const written = Object.fromEntries( - storedValues(original) - .filter((entry): entry is { key: string; value: string; secure: boolean } => entry.value !== null) + [...secureValues(original), ...plainValues(original)] + .filter((entry): entry is { key: string; value: string } => entry.value !== null) .map(({ key, value }) => [key, value]), ); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat/playgroundSettings.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat/playgroundSettings.ts index b502702308b..9fad492ae77 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat/playgroundSettings.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat/playgroundSettings.ts @@ -29,7 +29,7 @@ export type PlaygroundSettings = Readonly<{ maxTokens: number | null; }>; -export type StoredValue = Readonly<{ key: string; value: string | null; secure: boolean }>; +export type StoredValue = Readonly<{ key: string; value: string | null }>; export type SettingsStorage = Readonly<{ read: (key: string) => string | null; @@ -122,21 +122,24 @@ export const loadSettings = (storage: SettingsStorage, keysDisabled: boolean): P stream: storage.read("streamingEnabled") !== "false", }); -export const storedValues = (settings: PlaygroundSettings): readonly StoredValue[] => [ - { key: "apiKeySource", value: JSON.stringify(settings.keySource), secure: true }, - { key: "apiKey", value: settings.virtualKey, secure: true }, - { key: "customHeaders", value: JSON.stringify(settings.headerPairs), secure: true }, - { key: "customProxyBaseUrl", value: settings.proxyBaseUrl || null, secure: false }, - { key: "endpointType", value: settings.endpoint, secure: false }, - { key: "selectedModel", value: settings.model, secure: false }, - { key: "selectedVoice", value: settings.voice, secure: false }, - { key: "selectedTags", value: JSON.stringify(settings.tags), secure: false }, - { key: "selectedVectorStores", value: JSON.stringify(settings.vectorStores), secure: false }, - { key: "selectedGuardrails", value: JSON.stringify(settings.guardrails), secure: false }, - { key: "selectedPolicies", value: JSON.stringify(settings.policies), secure: false }, - { key: "selectedMCPServers", value: JSON.stringify(settings.mcpSelection), secure: false }, - { key: "mcpServerToolRestrictions", value: JSON.stringify(settings.toolLimits), secure: false }, - { key: "streamingEnabled", value: String(settings.stream), secure: false }, +export const secureValues = (settings: PlaygroundSettings): readonly StoredValue[] => [ + { key: "apiKeySource", value: JSON.stringify(settings.keySource) }, + { key: "apiKey", value: settings.virtualKey }, + { key: "customHeaders", value: JSON.stringify(settings.headerPairs) }, +]; + +export const plainValues = (settings: PlaygroundSettings): readonly StoredValue[] => [ + { key: "customProxyBaseUrl", value: settings.proxyBaseUrl || null }, + { key: "endpointType", value: settings.endpoint }, + { key: "selectedModel", value: settings.model }, + { key: "selectedVoice", value: settings.voice }, + { key: "selectedTags", value: JSON.stringify(settings.tags) }, + { key: "selectedVectorStores", value: JSON.stringify(settings.vectorStores) }, + { key: "selectedGuardrails", value: JSON.stringify(settings.guardrails) }, + { key: "selectedPolicies", value: JSON.stringify(settings.policies) }, + { key: "selectedMCPServers", value: JSON.stringify(settings.mcpSelection) }, + { key: "mcpServerToolRestrictions", value: JSON.stringify(settings.toolLimits) }, + { key: "streamingEnabled", value: String(settings.stream) }, ]; const singleServer = (selection: readonly string[]): readonly string[] => diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat/usePlaygroundSettings.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat/usePlaygroundSettings.ts index 67c485bf56f..9a8281422c3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat/usePlaygroundSettings.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat/usePlaygroundSettings.ts @@ -1,6 +1,12 @@ import { useCallback, useEffect, useState } from "react"; import { getSecureItem, setSecureItem } from "@/utils/secureStorage"; -import { defaultSettings, loadSettings, storedValues, type PlaygroundSettings } from "./playgroundSettings"; +import { + defaultSettings, + loadSettings, + plainValues, + secureValues, + type PlaygroundSettings, +} from "./playgroundSettings"; export type SettingsUpdate = Partial | ((current: PlaygroundSettings) => PlaygroundSettings); @@ -10,10 +16,17 @@ const browserStorage = { }; const persist = (settings: PlaygroundSettings): void => { - storedValues(settings).forEach(({ key, value, secure }) => { + secureValues(settings).forEach(({ key, value }) => { + try { + if (value === null) window.sessionStorage.removeItem(key); + else setSecureItem(key, value); + } catch { + return; + } + }); + plainValues(settings).forEach(({ key, value }) => { try { if (value === null) window.sessionStorage.removeItem(key); - else if (secure) setSecureItem(key, value); else window.sessionStorage.setItem(key, value); } catch { return;