From 3088db3f0ec77fd8256eff2b70dee882755f9a4c Mon Sep 17 00:00:00 2001
From: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
Date: Sat, 29 Aug 2026 12:55:34 -0700
Subject: [PATCH] fix(prompts): accept a string prompt_version and carry the
viewed environment into code snippets
---
litellm/proxy/prompts/prompt_registry.py | 10 ++++++
litellm/proxy/utils.py | 5 +--
.../proxy/prompts/test_prompt_registry.py | 10 +++++-
.../proxy_logging/test_guardrail_pipeline.py | 35 ++++++++++++++++++
.../PromptCodeSnippets.test.tsx | 24 +++++++++++++
.../prompt_editor_view/PromptCodeSnippets.tsx | 29 ++++++++-------
.../PromptEditorHeader.test.tsx | 5 ++-
.../prompt_editor_view/PromptEditorHeader.tsx | 1 +
.../prompts/_components/prompt_info.test.tsx | 36 ++++++++++++++++++-
.../prompts/_components/prompt_info.tsx | 1 +
10 files changed, 139 insertions(+), 17 deletions(-)
diff --git a/litellm/proxy/prompts/prompt_registry.py b/litellm/proxy/prompts/prompt_registry.py
index ba53809690d..7803352e9e7 100644
--- a/litellm/proxy/prompts/prompt_registry.py
+++ b/litellm/proxy/prompts/prompt_registry.py
@@ -74,6 +74,16 @@ def registry_key_for_prompt(prompt: PromptSpec) -> str:
return f"{prompt.prompt_id}::{prompt_environment_or_default(prompt.environment)}"
+def parse_prompt_version(raw_version: object) -> int | None:
+ if isinstance(raw_version, bool):
+ return None
+ if isinstance(raw_version, int):
+ return raw_version
+ if isinstance(raw_version, str) and raw_version.isdigit():
+ return int(raw_version)
+ return None
+
+
def _spec_version(prompt: PromptSpec) -> int:
return prompt.version if prompt.version is not None else get_version_number(prompt_id=prompt.prompt_id)
diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py
index 09efa0cc72d..0263ce7598a 100644
--- a/litellm/proxy/utils.py
+++ b/litellm/proxy/utils.py
@@ -1749,7 +1749,6 @@ class ProxyLogging:
litellm_logging_obj: Final = cast(Optional["LiteLLMLoggingObj"], data.get("litellm_logging_obj", None))
prompt_id: Final[str | None] = data.get("prompt_id", None)
- prompt_version: Final[int | None] = data.get("prompt_version", None)
## PROMPT TEMPLATE CHECK ##
@@ -1759,11 +1758,13 @@ class ProxyLogging:
and prompt_id is not None
and (call_type == "completion" or call_type == "acompletion" or call_type == "aresponses")
):
+ from litellm.proxy.prompts.prompt_registry import parse_prompt_version
+
await self._process_prompt_template(
data=data,
litellm_logging_obj=litellm_logging_obj,
prompt_id=prompt_id,
- prompt_version=prompt_version,
+ prompt_version=parse_prompt_version(data.get("prompt_version", None)),
call_type=call_type,
)
diff --git a/tests/test_litellm/proxy/prompts/test_prompt_registry.py b/tests/test_litellm/proxy/prompts/test_prompt_registry.py
index 32930fbbdff..a0743b65dd7 100644
--- a/tests/test_litellm/proxy/prompts/test_prompt_registry.py
+++ b/tests/test_litellm/proxy/prompts/test_prompt_registry.py
@@ -2,7 +2,7 @@ import pytest
import litellm
from litellm.integrations.custom_prompt_management import CustomPromptManagement
-from litellm.proxy.prompts.prompt_registry import InMemoryPromptRegistry
+from litellm.proxy.prompts.prompt_registry import InMemoryPromptRegistry, parse_prompt_version
from litellm.types.prompts.init_prompts import PromptInfo, PromptLiteLLMParams, PromptSpec
@@ -214,3 +214,11 @@ def test_remove_prompt_is_a_no_op_for_an_unknown_registry_key(isolated_callbacks
assert registry.resolve_prompt_spec("greeting") is not None
assert len(isolated_callbacks) == 1
+
+
+@pytest.mark.parametrize(
+ ("raw_version", "expected"),
+ [(2, 2), ("2", 2), (None, None), ("v2", None), (True, None), (2.0, None)],
+)
+def test_parse_prompt_version_accepts_integers_and_json_strings(raw_version: object, expected: int | None) -> None:
+ assert parse_prompt_version(raw_version) == expected
diff --git a/tests/test_litellm/proxy/utils/proxy_logging/test_guardrail_pipeline.py b/tests/test_litellm/proxy/utils/proxy_logging/test_guardrail_pipeline.py
index 6589b14e53a..2ba58bd5644 100644
--- a/tests/test_litellm/proxy/utils/proxy_logging/test_guardrail_pipeline.py
+++ b/tests/test_litellm/proxy/utils/proxy_logging/test_guardrail_pipeline.py
@@ -858,6 +858,41 @@ async def test_process_prompt_template_resolves_the_requested_environment(proxy_
assert data["messages"] == [{"role": "user", "content": "rendered"}]
+@pytest.mark.asyncio
+async def test_pre_call_hook_matches_a_prompt_version_sent_as_a_json_string(proxy_logging, monkeypatch):
+ from litellm.proxy._types import UserAPIKeyAuth
+ from litellm.proxy.prompts import prompt_registry
+
+ prompt_spec = MagicMock()
+ prompt_spec.litellm_params = MagicMock(prompt_id="greeting")
+ resolve_calls: list[dict] = []
+
+ def fake_resolve(prompt_id, version=None, environment=None):
+ resolve_calls.append({"prompt_id": prompt_id, "version": version, "environment": environment})
+ return prompt_spec
+
+ monkeypatch.setattr(prompt_registry.IN_MEMORY_PROMPT_REGISTRY, "resolve_prompt_spec", fake_resolve)
+ monkeypatch.setattr(
+ prompt_registry.IN_MEMORY_PROMPT_REGISTRY, "get_prompt_callback_for_prompt", lambda *a, **kw: MagicMock()
+ )
+ logging_obj = MagicMock()
+ logging_obj.async_get_chat_completion_prompt = AsyncMock(
+ return_value=("m", [{"role": "user", "content": "rendered"}], {})
+ )
+ data: Dict[str, Any] = {
+ "messages": [{"role": "user", "content": "orig"}],
+ "model": "m",
+ "prompt_id": "greeting",
+ "prompt_version": "2",
+ "litellm_logging_obj": logging_obj,
+ }
+
+ result = await proxy_logging.pre_call_hook(user_api_key_dict=UserAPIKeyAuth(), data=data, call_type="completion")
+
+ assert resolve_calls == [{"prompt_id": "greeting", "version": 2, "environment": None}]
+ assert result["messages"] == [{"role": "user", "content": "rendered"}]
+
+
@pytest.mark.asyncio
async def test_process_prompt_template_aresponses_swaps_model_and_merges_input(proxy_logging, monkeypatch):
from litellm.proxy.prompts import prompt_registry
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: () => }));
+vi.mock("./PromptCodeSnippets", () => ({
+ default: ({ environment }: { environment?: string }) => ,
+}));
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 a5b195e5057..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 = {
@@ -58,6 +60,38 @@ describe("PromptInfoView environment scoping", () => {
});
});
+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 af4e3bf2121..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
@@ -221,6 +221,7 @@ const PromptInfoView: React.FC = ({
promptVariables={extractTemplateVariables(promptTemplate?.content)}
accessToken={accessToken}
version={currentVersion}
+ environment={selectedEnv ?? promptData.environment}
/>