mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-18 04:44:37 +00:00
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
This commit is contained in:
parent
e547be51a5
commit
ca4e742115
4 changed files with 54 additions and 24 deletions
|
|
@ -6,7 +6,7 @@ import { makeOpenAIChatCompletionRequest } from "@/components/llm_calls/chat_com
|
||||||
import { fetchAvailableModels } from "@/components/llm_calls/fetch_models";
|
import { fetchAvailableModels } from "@/components/llm_calls/fetch_models";
|
||||||
import { makeOpenAIResponsesRequest } from "@/components/llm_calls/responses_api";
|
import { makeOpenAIResponsesRequest } from "@/components/llm_calls/responses_api";
|
||||||
import { callMCPTool, fetchMCPServers, fetchMCPToolsets, listMCPTools } from "@/components/networking";
|
import { callMCPTool, fetchMCPServers, fetchMCPToolsets, listMCPTools } from "@/components/networking";
|
||||||
import { setSecureItem } from "@/utils/secureStorage";
|
import { getSecureItem, setSecureItem } from "@/utils/secureStorage";
|
||||||
import { PlaygroundChat } from "./PlaygroundChat";
|
import { PlaygroundChat } from "./PlaygroundChat";
|
||||||
|
|
||||||
vi.mock("@/app/(dashboard)/hooks/useCan", () => ({ default: () => true }));
|
vi.mock("@/app/(dashboard)/hooks/useCan", () => ({ default: () => true }));
|
||||||
|
|
@ -487,4 +487,17 @@ describe("PlaygroundChat", () => {
|
||||||
|
|
||||||
expect(screen.queryByText("Python Code Executed")).not.toBeInTheDocument();
|
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([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ import {
|
||||||
allowedTools,
|
allowedTools,
|
||||||
defaultSettings,
|
defaultSettings,
|
||||||
loadSettings,
|
loadSettings,
|
||||||
storedValues,
|
plainValues,
|
||||||
|
secureValues,
|
||||||
toggleMcpItem,
|
toggleMcpItem,
|
||||||
toggleToolLimit,
|
toggleToolLimit,
|
||||||
withEndpoint,
|
withEndpoint,
|
||||||
|
|
@ -29,7 +30,7 @@ const TOOLS = ["search", "fetch", "summarize"] as const;
|
||||||
const SCOPE = { server: "s", tools: TOOLS };
|
const SCOPE = { server: "s", tools: TOOLS };
|
||||||
|
|
||||||
describe("loadSettings", () => {
|
describe("loadSettings", () => {
|
||||||
it("restores every persisted field from what storedValues wrote", () => {
|
it("restores every persisted field from what was written", () => {
|
||||||
const persisted: Partial<PlaygroundSettings> = {
|
const persisted: Partial<PlaygroundSettings> = {
|
||||||
keySource: "custom",
|
keySource: "custom",
|
||||||
virtualKey: "sk-test",
|
virtualKey: "sk-test",
|
||||||
|
|
@ -48,8 +49,8 @@ describe("loadSettings", () => {
|
||||||
};
|
};
|
||||||
const original = settingsWith(persisted);
|
const original = settingsWith(persisted);
|
||||||
const written = Object.fromEntries(
|
const written = Object.fromEntries(
|
||||||
storedValues(original)
|
[...secureValues(original), ...plainValues(original)]
|
||||||
.filter((entry): entry is { key: string; value: string; secure: boolean } => entry.value !== null)
|
.filter((entry): entry is { key: string; value: string } => entry.value !== null)
|
||||||
.map(({ key, value }) => [key, value]),
|
.map(({ key, value }) => [key, value]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export type PlaygroundSettings = Readonly<{
|
||||||
maxTokens: number | null;
|
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<{
|
export type SettingsStorage = Readonly<{
|
||||||
read: (key: string) => string | null;
|
read: (key: string) => string | null;
|
||||||
|
|
@ -122,21 +122,24 @@ export const loadSettings = (storage: SettingsStorage, keysDisabled: boolean): P
|
||||||
stream: storage.read("streamingEnabled") !== "false",
|
stream: storage.read("streamingEnabled") !== "false",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const storedValues = (settings: PlaygroundSettings): readonly StoredValue[] => [
|
export const secureValues = (settings: PlaygroundSettings): readonly StoredValue[] => [
|
||||||
{ key: "apiKeySource", value: JSON.stringify(settings.keySource), secure: true },
|
{ key: "apiKeySource", value: JSON.stringify(settings.keySource) },
|
||||||
{ key: "apiKey", value: settings.virtualKey, secure: true },
|
{ key: "apiKey", value: settings.virtualKey },
|
||||||
{ key: "customHeaders", value: JSON.stringify(settings.headerPairs), secure: true },
|
{ key: "customHeaders", value: JSON.stringify(settings.headerPairs) },
|
||||||
{ key: "customProxyBaseUrl", value: settings.proxyBaseUrl || null, secure: false },
|
];
|
||||||
{ key: "endpointType", value: settings.endpoint, secure: false },
|
|
||||||
{ key: "selectedModel", value: settings.model, secure: false },
|
export const plainValues = (settings: PlaygroundSettings): readonly StoredValue[] => [
|
||||||
{ key: "selectedVoice", value: settings.voice, secure: false },
|
{ key: "customProxyBaseUrl", value: settings.proxyBaseUrl || null },
|
||||||
{ key: "selectedTags", value: JSON.stringify(settings.tags), secure: false },
|
{ key: "endpointType", value: settings.endpoint },
|
||||||
{ key: "selectedVectorStores", value: JSON.stringify(settings.vectorStores), secure: false },
|
{ key: "selectedModel", value: settings.model },
|
||||||
{ key: "selectedGuardrails", value: JSON.stringify(settings.guardrails), secure: false },
|
{ key: "selectedVoice", value: settings.voice },
|
||||||
{ key: "selectedPolicies", value: JSON.stringify(settings.policies), secure: false },
|
{ key: "selectedTags", value: JSON.stringify(settings.tags) },
|
||||||
{ key: "selectedMCPServers", value: JSON.stringify(settings.mcpSelection), secure: false },
|
{ key: "selectedVectorStores", value: JSON.stringify(settings.vectorStores) },
|
||||||
{ key: "mcpServerToolRestrictions", value: JSON.stringify(settings.toolLimits), secure: false },
|
{ key: "selectedGuardrails", value: JSON.stringify(settings.guardrails) },
|
||||||
{ key: "streamingEnabled", value: String(settings.stream), secure: false },
|
{ 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[] =>
|
const singleServer = (selection: readonly string[]): readonly string[] =>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { getSecureItem, setSecureItem } from "@/utils/secureStorage";
|
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<PlaygroundSettings> | ((current: PlaygroundSettings) => PlaygroundSettings);
|
export type SettingsUpdate = Partial<PlaygroundSettings> | ((current: PlaygroundSettings) => PlaygroundSettings);
|
||||||
|
|
||||||
|
|
@ -10,10 +16,17 @@ const browserStorage = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const persist = (settings: PlaygroundSettings): void => {
|
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 {
|
try {
|
||||||
if (value === null) window.sessionStorage.removeItem(key);
|
if (value === null) window.sessionStorage.removeItem(key);
|
||||||
else if (secure) setSecureItem(key, value);
|
|
||||||
else window.sessionStorage.setItem(key, value);
|
else window.sessionStorage.setItem(key, value);
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue