;
isAdmin: boolean;
- onPromptClick?: (id: string) => void;
- onDeleteClick?: (id: string, name: string) => void;
+ onPromptClick?: (id: string, environment: string) => void;
+ onDeleteClick?: (id: string, name: string, environment: string) => void;
}
export const getPromptTableColumns = ({
@@ -128,7 +134,11 @@ export const getPromptTableColumns = ({
title={row.original.prompt_id}
titleClassName="font-mono text-xs font-normal"
className="max-w-60"
- onClick={onPromptClick ? () => onPromptClick(row.original.prompt_id) : undefined}
+ onClick={
+ onPromptClick
+ ? () => onPromptClick(row.original.prompt_id, row.original.environment || "development")
+ : undefined
+ }
/>
),
},
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.test.tsx
index 3b0ee3a3ce2..a1c7280422f 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.test.tsx
@@ -16,21 +16,31 @@ vi.mock("./PromptTable", () => ({
__esModule: true,
default: ({
isLoading,
+ onPromptClick,
onDeleteClick,
}: {
isLoading: boolean;
- onDeleteClick: (id: string, name: string) => void;
+ onPromptClick: (id: string, environment: string) => void;
+ onDeleteClick: (id: string, name: string, environment: string) => void;
}) => (
{isLoading ? "table-loading" : "table-loaded"}
-
),
}));
-vi.mock("./prompt_info", () => ({ __esModule: true, default: () => prompt-info-view
}));
+vi.mock("./prompt_info", () => ({
+ __esModule: true,
+ default: ({ initialEnvironment }: { initialEnvironment?: string }) => (
+ prompt-info-view:{initialEnvironment ?? "none"}
+ ),
+}));
vi.mock("./add_prompt_form", () => ({
__esModule: true,
default: ({ visible }: { visible: boolean }) => (visible ? add-prompt-form
: null),
@@ -141,6 +151,22 @@ describe("PromptsPanel toolbar", () => {
});
});
+describe("PromptsPanel row navigation", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockGetPromptsList.mockResolvedValue({ prompts: [] } as never);
+ });
+
+ it("should open the info view preselected to the clicked row's environment", async () => {
+ const user = userEvent.setup();
+ renderPanel("Admin");
+
+ await user.click(await screen.findByRole("button", { name: "row-open" }));
+
+ expect(screen.getByText("prompt-info-view:staging")).toBeInTheDocument();
+ });
+});
+
describe("PromptsPanel delete confirmation", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -154,13 +180,13 @@ describe("PromptsPanel delete confirmation", () => {
await user.click(await screen.findByRole("button", { name: "row-delete" }));
- expect(await screen.findByText(/delete prompt: my-prompt/i)).toBeInTheDocument();
+ expect(await screen.findByText(/the staging copy of prompt: my-prompt/i)).toBeInTheDocument();
expect(screen.getByText(/cannot be undone/i)).toBeInTheDocument();
expect(mockDeletePromptCall).not.toHaveBeenCalled();
await user.click(screen.getByRole("button", { name: /^delete$/i }));
- await waitFor(() => expect(mockDeletePromptCall).toHaveBeenCalledWith("sk-test", "prompt-1"));
+ await waitFor(() => expect(mockDeletePromptCall).toHaveBeenCalledWith("sk-test", "prompt-1", "staging"));
});
it("should abandon the delete when the confirmation is dismissed", async () => {
@@ -168,11 +194,11 @@ describe("PromptsPanel delete confirmation", () => {
renderPanel("Admin");
await user.click(await screen.findByRole("button", { name: "row-delete" }));
- await screen.findByText(/delete prompt: my-prompt/i);
+ await screen.findByText(/the staging copy of prompt: my-prompt/i);
await user.click(screen.getByRole("button", { name: /cancel/i }));
- await waitFor(() => expect(screen.queryByText(/delete prompt: my-prompt/i)).not.toBeInTheDocument());
+ await waitFor(() => expect(screen.queryByText(/the staging copy of prompt: my-prompt/i)).not.toBeInTheDocument());
expect(mockDeletePromptCall).not.toHaveBeenCalled();
});
@@ -187,14 +213,14 @@ describe("PromptsPanel delete confirmation", () => {
renderPanel("Admin");
await user.click(await screen.findByRole("button", { name: "row-delete" }));
- await screen.findByText(/delete prompt: my-prompt/i);
+ await screen.findByText(/the staging copy of prompt: my-prompt/i);
await user.click(screen.getByRole("button", { name: /^delete$/i }));
- await waitFor(() => expect(mockDeletePromptCall).toHaveBeenCalledWith("sk-test", "prompt-1"));
+ await waitFor(() => expect(mockDeletePromptCall).toHaveBeenCalledWith("sk-test", "prompt-1", "staging"));
await user.keyboard("{Escape}");
- expect(screen.getByText(/delete prompt: my-prompt/i)).toBeInTheDocument();
+ expect(screen.getByText(/the staging copy of prompt: my-prompt/i)).toBeInTheDocument();
finishDelete();
- await waitFor(() => expect(screen.queryByText(/delete prompt: my-prompt/i)).not.toBeInTheDocument());
+ await waitFor(() => expect(screen.queryByText(/the staging copy of prompt: my-prompt/i)).not.toBeInTheDocument());
});
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.tsx
index 3594e933704..2d8d905c480 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/index.tsx
@@ -41,11 +41,12 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
const [isLoading, setIsLoading] = useState(true);
const [selectedEnvironment, setSelectedEnvironment] = useState(undefined);
const [selectedPromptId, setSelectedPromptId] = useState(null);
+ const [selectedPromptEnvironment, setSelectedPromptEnvironment] = useState(undefined);
const [isAddModalVisible, setIsAddModalVisible] = useState(false);
const [showEditorView, setShowEditorView] = useState(false);
const [editPromptData, setEditPromptData] = useState(null);
const [isDeleting, setIsDeleting] = useState(false);
- const [promptToDelete, setPromptToDelete] = useState<{ id: string; name: string } | null>(null);
+ const [promptToDelete, setPromptToDelete] = useState<{ id: string; name: string; environment: string } | null>(null);
// Admin Viewer follows the read-parity rule: see prompts, no writes.
const canModify = userRole ? isProxyAdminRole(userRole) : false;
@@ -71,8 +72,9 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
fetchPrompts();
}, [accessToken, selectedEnvironment]);
- const handlePromptClick = (promptId: string) => {
+ const handlePromptClick = (promptId: string, environment: string) => {
setSelectedPromptId(promptId);
+ setSelectedPromptEnvironment(environment);
};
const handleAddPrompt = () => {
@@ -111,8 +113,8 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
setSelectedPromptId(null);
};
- const handleDeleteClick = (promptId: string, promptName: string) => {
- setPromptToDelete({ id: promptId, name: promptName });
+ const handleDeleteClick = (promptId: string, promptName: string, environment: string) => {
+ setPromptToDelete({ id: promptId, name: promptName, environment });
};
const handleDeleteConfirm = async () => {
@@ -120,8 +122,8 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
setIsDeleting(true);
try {
- await deletePromptCall(accessToken, promptToDelete.id);
- toast.success(`Prompt "${promptToDelete.name}" deleted successfully`);
+ await deletePromptCall(accessToken, promptToDelete.id, promptToDelete.environment);
+ toast.success(`Prompt "${promptToDelete.name}" deleted successfully from ${promptToDelete.environment}`);
fetchPrompts(); // Refresh the list
} catch (error) {
console.error("Error deleting prompt:", error);
@@ -148,6 +150,7 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
) : selectedPromptId ? (
setSelectedPromptId(null)}
accessToken={accessToken}
isAdmin={canModify}
@@ -219,7 +222,8 @@ const PromptsPanel: React.FC = ({ accessToken, userRole }) => {
Delete Prompt
- Are you sure you want to delete prompt: {promptToDelete.name} ? This action cannot be undone.
+ Are you sure you want to delete the {promptToDelete.environment} copy of prompt: {promptToDelete.name}?
+ This action cannot be undone.
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptCodeSnippets.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptCodeSnippets.test.tsx
index a1b4ad52634..7fa44a4dfe5 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptCodeSnippets.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptCodeSnippets.test.tsx
@@ -44,4 +44,28 @@ describe("PromptCodeSnippets", () => {
expect(screen.getByRole("combobox", { name: "Language" })).toHaveTextContent("Python (OpenAI SDK)");
});
+
+ it("includes the viewed environment in every generated request", async () => {
+ const user = userEvent.setup({ pointerEventsCheck: PointerEventsCheckLevel.Never });
+ render(
+ ,
+ );
+ await user.click(screen.getByRole("button", { name: /get code/i }));
+ await screen.findByText("Generated Code");
+
+ await user.click(screen.getByRole("button", { name: /copy to clipboard/i }));
+ expect(await navigator.clipboard.readText()).toContain('"prompt_environment": "development"');
+
+ await user.click(screen.getByRole("tab", { name: "With Version" }));
+ await user.click(screen.getByRole("button", { name: /copy to clipboard/i }));
+ const versionSnippet = await navigator.clipboard.readText();
+ expect(versionSnippet).toContain('"prompt_environment": "development"');
+ expect(versionSnippet).toContain('"prompt_version": 2');
+ });
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptCodeSnippets.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptCodeSnippets.tsx
index af7d6421265..a6adc160674 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptCodeSnippets.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptCodeSnippets.tsx
@@ -22,6 +22,7 @@ interface PromptCodeSnippetsProps {
promptVariables?: Record;
accessToken: string | null;
version?: string;
+ environment?: string;
proxySettings?: {
PROXY_BASE_URL?: string;
LITELLM_UI_API_DOC_BASE_URL?: string | null;
@@ -34,6 +35,7 @@ const PromptCodeSnippets: React.FC = ({
promptVariables = {},
accessToken,
version = "1",
+ environment,
proxySettings,
}) => {
const syntaxTheme = useSyntaxTheme(coy);
@@ -64,6 +66,9 @@ const PromptCodeSnippets: React.FC = ({
// Generate code based on selected language and tab
const generateCode = () => {
const hasVariables = Object.keys(promptVariables).length > 0;
+ const curlEnvironment = environment ? `,\n "prompt_environment": "${environment}"` : "";
+ const pythonEnvironment = environment ? `,\n "prompt_environment": "${environment}"` : "";
+ const jsEnvironment = environment ? `,\n prompt_environment: "${environment}"` : "";
if (selectedLanguage === "curl") {
if (selectedTab === "basic") {
@@ -72,7 +77,7 @@ const PromptCodeSnippets: React.FC = ({
-H 'Authorization: Bearer ${effectiveApiKey}' \\
-d '{
"model": "${model}",
- "prompt_id": "${promptId}"${
+ "prompt_id": "${promptId}"${curlEnvironment}${
hasVariables
? `,
"prompt_variables": ${JSON.stringify(promptVariables, null, 6).replace(/\n/g, "\n ")}`
@@ -85,7 +90,7 @@ const PromptCodeSnippets: React.FC = ({
-H 'Authorization: Bearer ${effectiveApiKey}' \\
-d '{
"model": "${model}",
- "prompt_id": "${promptId}"${
+ "prompt_id": "${promptId}"${curlEnvironment}${
hasVariables
? `,
"prompt_variables": ${JSON.stringify(promptVariables, null, 6).replace(/\n/g, "\n ")}`
@@ -104,7 +109,7 @@ const PromptCodeSnippets: React.FC = ({
-H 'Authorization: Bearer ${effectiveApiKey}' \\
-d '{
"model": "${model}",
- "prompt_id": "${promptId}",
+ "prompt_id": "${promptId}"${curlEnvironment},
"prompt_version": ${version},
"messages": [
{
@@ -127,7 +132,7 @@ client = openai.OpenAI(
response = client.chat.completions.create(
model="${model}",
extra_body={
- "prompt_id": "${promptId}"${
+ "prompt_id": "${promptId}"${pythonEnvironment}${
hasVariables
? `,
"prompt_variables": ${JSON.stringify(promptVariables, null, 8).replace(/\n/g, "\n ")}`
@@ -145,7 +150,7 @@ response = client.chat.completions.create(
{"role": "user", "content": "hi"}
],
extra_body={
- "prompt_id": "${promptId}"${
+ "prompt_id": "${promptId}"${pythonEnvironment}${
hasVariables
? `,
"prompt_variables": ${JSON.stringify(promptVariables, null, 8).replace(/\n/g, "\n ")}`
@@ -163,7 +168,7 @@ response = client.chat.completions.create(
{"role": "user", "content": "Who are u"}
],
extra_body={
- "prompt_id": "${promptId}",
+ "prompt_id": "${promptId}"${pythonEnvironment},
"prompt_version": ${version}
}
)
@@ -186,9 +191,9 @@ async function main() {
model: "${model}",
${
hasVariables
- ? `prompt_id: "${promptId}",
+ ? `prompt_id: "${promptId}"${jsEnvironment},
prompt_variables: ${JSON.stringify(promptVariables, null, 8).replace(/\n/g, "\n ")}`
- : `prompt_id: "${promptId}"`
+ : `prompt_id: "${promptId}"${jsEnvironment}`
}
});
@@ -206,9 +211,9 @@ async function main() {
],
${
hasVariables
- ? `prompt_id: "${promptId}",
+ ? `prompt_id: "${promptId}"${jsEnvironment},
prompt_variables: ${JSON.stringify(promptVariables, null, 8).replace(/\n/g, "\n ")}`
- : `prompt_id: "${promptId}"`
+ : `prompt_id: "${promptId}"${jsEnvironment}`
}
});
@@ -224,7 +229,7 @@ async function main() {
messages: [
{ role: "user", content: "Who are u" }
],
- prompt_id: "${promptId}",
+ prompt_id: "${promptId}"${jsEnvironment},
prompt_version: ${version}
});
@@ -241,7 +246,7 @@ main();`;
if (isModalVisible) {
setGeneratedCode(generateCode());
}
- }, [isModalVisible, selectedLanguage, selectedTab, promptId, model, promptVariables]);
+ }, [isModalVisible, selectedLanguage, selectedTab, promptId, model, promptVariables, version, environment]);
return (
<>
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptEditorHeader.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptEditorHeader.test.tsx
index 5194cdd4e63..3afc00e37a5 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptEditorHeader.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptEditorHeader.test.tsx
@@ -2,7 +2,9 @@ import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import PromptEditorHeader from "./PromptEditorHeader";
-vi.mock("./PromptCodeSnippets", () => ({ default: () => Get Code }));
+vi.mock("./PromptCodeSnippets", () => ({
+ default: ({ environment }: { environment?: string }) => Get Code,
+}));
describe("PromptEditorHeader", () => {
it("preserves navigation, naming, and save actions", () => {
@@ -48,5 +50,6 @@ describe("PromptEditorHeader", () => {
);
expect(screen.getByRole("combobox", { name: "Environment" })).toHaveTextContent(label);
+ expect(screen.getByRole("button", { name: "Get Code" })).toHaveAttribute("data-environment", environment);
});
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptEditorHeader.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptEditorHeader.tsx
index eea9755054f..04cac01365a 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptEditorHeader.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_editor_view/PromptEditorHeader.tsx
@@ -89,6 +89,7 @@ const PromptEditorHeader: React.FC = ({
promptVariables={promptVariables}
accessToken={accessToken}
version={version?.replace("v", "") || "1"}
+ environment={environment}
proxySettings={proxySettings}
/>
{editMode && onShowHistory && (
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.test.tsx
index bb29f12ac42..b14d5c3d91f 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.test.tsx
@@ -12,7 +12,9 @@ vi.mock("@/components/networking", () => ({
}));
vi.mock("./prompt_editor_view/PromptCodeSnippets", () => ({
- default: () => ,
+ default: ({ environment }: { environment?: string }) => (
+
+ ),
}));
const promptWithoutTemplate = {
@@ -29,6 +31,67 @@ const promptWithoutTemplate = {
environments: [],
};
+describe("PromptInfoView environment scoping", () => {
+ beforeEach(() => {
+ vi.mocked(networking.getPromptInfo).mockReset().mockResolvedValue(promptWithoutTemplate);
+ vi.mocked(networking.getPromptVersions).mockReset().mockResolvedValue({ prompts: [] });
+ });
+
+ it("fetches the initial environment it was opened with", async () => {
+ render(
+ ,
+ );
+
+ await screen.findByRole("tab", { name: "Raw JSON" });
+ expect(networking.getPromptInfo).toHaveBeenCalledWith("sk-test", "support-reply", "staging");
+ });
+
+ it("fetches the serve default when opened without an environment", async () => {
+ render();
+
+ await screen.findByRole("tab", { name: "Raw JSON" });
+ expect(networking.getPromptInfo).toHaveBeenCalledWith("sk-test", "support-reply", undefined);
+ });
+});
+
+describe("PromptInfoView code snippets", () => {
+ beforeEach(() => {
+ vi.mocked(networking.getPromptVersions).mockReset().mockResolvedValue({ prompts: [] });
+ });
+
+ it.each([
+ ["a prompt with several environments", "staging", ["development", "staging"]],
+ ["a config prompt with no environment list", "development", []],
+ ])("hands the viewed environment of %s to the code snippets", async (_label, environment, environments) => {
+ vi.mocked(networking.getPromptInfo)
+ .mockReset()
+ .mockResolvedValue({
+ ...promptWithoutTemplate,
+ prompt_spec: { ...promptWithoutTemplate.prompt_spec, environment },
+ environments,
+ });
+
+ render(
+ ,
+ );
+
+ await screen.findByRole("tab", { name: "Raw JSON" });
+ expect(screen.getByTestId("prompt-code-snippets")).toHaveAttribute("data-environment", environment);
+ });
+});
+
describe("PromptInfoView tabs", () => {
beforeEach(() => {
vi.mocked(networking.getPromptInfo).mockReset().mockResolvedValue(promptWithoutTemplate);
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.tsx b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.tsx
index c6a6f09fcaa..062e1d84a0a 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/prompts/_components/prompt_info.tsx
@@ -20,6 +20,7 @@ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "
export interface PromptInfoProps {
promptId: string;
+ initialEnvironment?: string;
onClose: () => void;
accessToken: string | null;
isAdmin: boolean;
@@ -27,7 +28,15 @@ export interface PromptInfoProps {
onEdit?: (promptData: any) => void;
}
-const PromptInfoView: React.FC = ({ promptId, onClose, accessToken, isAdmin, onDelete, onEdit }) => {
+const PromptInfoView: React.FC = ({
+ promptId,
+ initialEnvironment,
+ onClose,
+ accessToken,
+ isAdmin,
+ onDelete,
+ onEdit,
+}) => {
const [promptData, setPromptData] = useState(null);
const [promptTemplate, setPromptTemplate] = useState(null);
const [rawApiResponse, setRawApiResponse] = useState(null);
@@ -43,7 +52,7 @@ const PromptInfoView: React.FC = ({ promptId, onClose, accessTo
const [selectedVersion, setSelectedVersion] = useState(null);
const [loadingVersions, setLoadingVersions] = useState(false);
- // Initial fetch — no environment filter, gets default + all environments list
+ // Fetches the requested environment (or the serve-time default when omitted) plus the environments list
const fetchPromptInfo = async (environment?: string) => {
try {
setLoading(true);
@@ -89,7 +98,7 @@ const PromptInfoView: React.FC = ({ promptId, onClose, accessTo
setSelectedEnv(null);
setEnvironments([]);
setVersionHistory([]);
- fetchPromptInfo();
+ fetchPromptInfo(initialEnvironment);
}, [promptId, accessToken]);
// When environment changes (user clicks tab), re-fetch — skip initial mount
@@ -212,6 +221,7 @@ const PromptInfoView: React.FC = ({ promptId, onClose, accessTo
promptVariables={extractTemplateVariables(promptTemplate?.content)}
accessToken={accessToken}
version={currentVersion}
+ environment={selectedEnv ?? promptData.environment}
/>
onEdit?.(rawApiResponse)} className="flex items-center">
@@ -493,7 +503,7 @@ const PromptInfoView: React.FC = ({ promptId, onClose, accessTo
Delete Prompt
- Are you sure you want to delete prompt: {basePromptId}?
+ Are you sure you want to delete prompt: {basePromptId} from every environment?
This action cannot be undone.
diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx
index 7e0f6c7e4f5..02bce59d92d 100644
--- a/ui/litellm-dashboard/src/components/networking.tsx
+++ b/ui/litellm-dashboard/src/components/networking.tsx
@@ -4694,9 +4694,12 @@ export const updatePromptCall = async (accessToken: string, promptId: string, pr
}
};
-export const deletePromptCall = async (accessToken: string, promptId: string) => {
+export const deletePromptCall = async (accessToken: string, promptId: string, environment?: string) => {
try {
- const data = await apiClient.delete(`/prompts/${promptId}`, { accessToken });
+ const data = await apiClient.delete(`/prompts/${promptId}`, {
+ accessToken,
+ query: { environment: environment || undefined },
+ });
return data;
} catch (error) {
console.error("Failed to delete prompt:", error);
diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
index ee3a88acba0..7ada45947cb 100644
--- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
+++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
@@ -334,7 +334,7 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp
const fetchPrompts = async () => {
try {
const response = await getPromptsList(accessToken);
- setPromptsList(response.prompts.map((prompt) => prompt.prompt_id));
+ setPromptsList(Array.from(new Set(response.prompts.map((prompt) => prompt.prompt_id))));
} catch (error) {
console.error("Failed to fetch prompts:", error);
}
diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx
index 10b1983b54b..8b506329595 100644
--- a/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx
+++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx
@@ -425,6 +425,22 @@ describe("KeyEditView", () => {
expect(screen.getByText("Policies")).toBeInTheDocument();
});
+ it("lists a prompt existing in several environments once in the dropdown", async () => {
+ vi.mocked(getPromptsList).mockResolvedValueOnce({
+ prompts: [
+ { prompt_id: "envgreet", litellm_params: {}, prompt_info: { prompt_type: "db" }, environment: "development" },
+ { prompt_id: "envgreet", litellm_params: {}, prompt_info: { prompt_type: "db" }, environment: "production" },
+ ],
+ });
+
+ renderAs("Admin");
+
+ const prompts = await screen.findByLabelText(/Prompts/);
+ await userEvent.type(prompts, "envgreet");
+
+ expect(await screen.findAllByRole("option", { name: "envgreet" })).toHaveLength(1);
+ });
+
it("should omit both fields and fire neither admin-only request for an internal user", async () => {
renderAs("Internal User");
diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx
index 1330be2788b..9edfba4b981 100644
--- a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx
+++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx
@@ -165,7 +165,7 @@ export function KeyEditView({
if (!accessToken) return;
try {
const response = await getPromptsList(accessToken);
- setPromptsList(response.prompts.map((prompt) => prompt.prompt_id));
+ setPromptsList(Array.from(new Set(response.prompts.map((prompt) => prompt.prompt_id))));
} catch (error) {
console.error("Failed to fetch prompts:", error);
}