mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-06 08:16:43 +00:00
fix(prompts): Prompt Studio fails to load prompts saved via UI
When prompts are saved via the Prompt Studio UI or PUT /prompts/{id} API
using prompt_data, the dotprompt_content field is null. The Prompt Studio
load path previously only read from dotprompt_content, causing the editor
to show a blank "New prompt" instead of the saved content.
This fix adds a fallback to parse prompt_data.content and
prompt_data.metadata when dotprompt_content is not available.
Changes:
- Modified parseExistingPrompt to check for prompt_data as fallback
- Added parsePromptData helper function to handle both direct and nested
prompt_data formats
- Updated error message to mention both data sources
- Added comprehensive tests for prompt_data parsing
Fixes #23935
This commit is contained in:
parent
cec3e9e7d4
commit
4930c6d542
2 changed files with 216 additions and 3 deletions
|
|
@ -339,7 +339,7 @@ User: Hello`,
|
|||
},
|
||||
};
|
||||
|
||||
expect(() => parseExistingPrompt(apiResponse)).toThrow("No dotprompt_content found in API response");
|
||||
expect(() => parseExistingPrompt(apiResponse)).toThrow("No dotprompt_content or prompt_data found in API response");
|
||||
});
|
||||
|
||||
it("should throw error for invalid dotprompt format", () => {
|
||||
|
|
@ -377,6 +377,143 @@ output:
|
|||
{ role: "user", content: "Enter task specifics. Use {{template_variables}} for dynamic inputs" },
|
||||
]);
|
||||
});
|
||||
|
||||
// Tests for prompt_data fallback
|
||||
it("should parse prompt_data with direct format", () => {
|
||||
const apiResponse = {
|
||||
prompt_spec: {
|
||||
litellm_params: {
|
||||
prompt_data: {
|
||||
content: "User: Hello from prompt_data",
|
||||
metadata: {
|
||||
model: "gpt-4",
|
||||
temperature: 0.8,
|
||||
},
|
||||
},
|
||||
},
|
||||
prompt_id: "test-prompt",
|
||||
},
|
||||
};
|
||||
|
||||
const result = parseExistingPrompt(apiResponse);
|
||||
expect(result.name).toBe("test-prompt");
|
||||
expect(result.model).toBe("gpt-4");
|
||||
expect(result.config.temperature).toBe(0.8);
|
||||
expect(result.messages).toEqual([{ role: "user", content: "Hello from prompt_data" }]);
|
||||
});
|
||||
|
||||
it("should parse prompt_data with nested format", () => {
|
||||
const apiResponse = {
|
||||
prompt_spec: {
|
||||
litellm_params: {
|
||||
prompt_data: {
|
||||
"my-prompt": {
|
||||
content: "User: Hello from nested prompt_data",
|
||||
metadata: {
|
||||
model: "gpt-3.5-turbo",
|
||||
max_tokens: 500,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
prompt_id: "my-prompt",
|
||||
},
|
||||
};
|
||||
|
||||
const result = parseExistingPrompt(apiResponse);
|
||||
expect(result.name).toBe("my-prompt");
|
||||
expect(result.model).toBe("gpt-3.5-turbo");
|
||||
expect(result.config.max_tokens).toBe(500);
|
||||
expect(result.messages).toEqual([{ role: "user", content: "Hello from nested prompt_data" }]);
|
||||
});
|
||||
|
||||
it("should prefer dotprompt_content over prompt_data", () => {
|
||||
const apiResponse = {
|
||||
prompt_spec: {
|
||||
litellm_params: {
|
||||
dotprompt_content: `---
|
||||
model: gpt-4
|
||||
input:
|
||||
schema:
|
||||
output:
|
||||
format: text
|
||||
---
|
||||
|
||||
User: Hello from dotprompt`,
|
||||
prompt_data: {
|
||||
content: "User: Hello from prompt_data",
|
||||
metadata: { model: "gpt-3.5-turbo" },
|
||||
},
|
||||
},
|
||||
prompt_id: "test-prompt",
|
||||
},
|
||||
};
|
||||
|
||||
const result = parseExistingPrompt(apiResponse);
|
||||
// Should use dotprompt_content, not prompt_data
|
||||
expect(result.model).toBe("gpt-4");
|
||||
expect(result.messages).toEqual([{ role: "user", content: "Hello from dotprompt" }]);
|
||||
});
|
||||
|
||||
it("should handle prompt_data with developer message", () => {
|
||||
const apiResponse = {
|
||||
prompt_spec: {
|
||||
litellm_params: {
|
||||
prompt_data: {
|
||||
content: "Developer: You are a helpful bot\n\nUser: Hello",
|
||||
metadata: { model: "gpt-4" },
|
||||
},
|
||||
},
|
||||
prompt_id: "test-prompt",
|
||||
},
|
||||
};
|
||||
|
||||
const result = parseExistingPrompt(apiResponse);
|
||||
expect(result.developerMessage).toBe("You are a helpful bot");
|
||||
expect(result.messages).toEqual([{ role: "user", content: "Hello" }]);
|
||||
});
|
||||
|
||||
it("should handle prompt_data with tools", () => {
|
||||
const apiResponse = {
|
||||
prompt_spec: {
|
||||
litellm_params: {
|
||||
prompt_data: {
|
||||
content: "User: What's the weather?",
|
||||
metadata: {
|
||||
model: "gpt-4",
|
||||
tools: [{ type: "function", function: { name: "get_weather", description: "Get weather info" } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
prompt_id: "test-prompt",
|
||||
},
|
||||
};
|
||||
|
||||
const result = parseExistingPrompt(apiResponse);
|
||||
expect(result.tools).toHaveLength(1);
|
||||
expect(result.tools[0].name).toBe("get_weather");
|
||||
expect(result.tools[0].description).toBe("Get weather info");
|
||||
});
|
||||
|
||||
it("should use default values for empty prompt_data", () => {
|
||||
const apiResponse = {
|
||||
prompt_spec: {
|
||||
litellm_params: {
|
||||
prompt_data: {
|
||||
content: "",
|
||||
metadata: {},
|
||||
},
|
||||
},
|
||||
prompt_id: "test-prompt",
|
||||
},
|
||||
};
|
||||
|
||||
const result = parseExistingPrompt(apiResponse);
|
||||
expect(result.model).toBe("gpt-4o");
|
||||
expect(result.messages).toEqual([
|
||||
{ role: "user", content: "Enter task specifics. Use {{template_variables}} for dynamic inputs" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getVersionNumber", () => {
|
||||
|
|
|
|||
|
|
@ -216,10 +216,21 @@ export const parseExistingPrompt = (apiResponse: any): PromptType => {
|
|||
// Extract dotprompt_content from litellm_params
|
||||
const dotpromptContent = apiResponse?.prompt_spec?.litellm_params?.dotprompt_content || "";
|
||||
|
||||
if (!dotpromptContent) {
|
||||
throw new Error("No dotprompt_content found in API response");
|
||||
// If dotprompt_content is available, use it
|
||||
if (dotpromptContent) {
|
||||
return parseDotpromptContent(dotpromptContent, apiResponse);
|
||||
}
|
||||
|
||||
// Fallback: try to construct from prompt_data
|
||||
const promptData = apiResponse?.prompt_spec?.litellm_params?.prompt_data;
|
||||
if (promptData) {
|
||||
return parsePromptData(promptData, apiResponse);
|
||||
}
|
||||
|
||||
throw new Error("No dotprompt_content or prompt_data found in API response");
|
||||
};
|
||||
|
||||
const parseDotpromptContent = (dotpromptContent: string, apiResponse: any): PromptType => {
|
||||
// Split into frontmatter and content
|
||||
const parts = dotpromptContent.split("---");
|
||||
if (parts.length < 3) {
|
||||
|
|
@ -249,6 +260,71 @@ export const parseExistingPrompt = (apiResponse: any): PromptType => {
|
|||
};
|
||||
};
|
||||
|
||||
const parsePromptData = (promptData: any, apiResponse: any): PromptType => {
|
||||
// Handle nested structure: { prompt_id: { content, metadata } } or { content, metadata }
|
||||
let content: string;
|
||||
let metadata: any;
|
||||
|
||||
if (promptData.content !== undefined) {
|
||||
// Direct format: { content, metadata }
|
||||
content = promptData.content || "";
|
||||
metadata = promptData.metadata || {};
|
||||
} else {
|
||||
// Nested format: { prompt_id: { content, metadata } }
|
||||
const keys = Object.keys(promptData);
|
||||
if (keys.length > 0) {
|
||||
const firstKey = keys[0];
|
||||
const nestedData = promptData[firstKey];
|
||||
content = nestedData?.content || "";
|
||||
metadata = nestedData?.metadata || {};
|
||||
} else {
|
||||
content = "";
|
||||
metadata = {};
|
||||
}
|
||||
}
|
||||
|
||||
// Strip version suffix from prompt name for display
|
||||
const promptId = apiResponse?.prompt_spec?.prompt_id || "Unnamed Prompt";
|
||||
const baseName = stripVersionFromPromptId(promptId) || promptId;
|
||||
|
||||
// Extract model from metadata
|
||||
const model = metadata?.model || "gpt-4o";
|
||||
|
||||
// Extract config parameters from metadata
|
||||
const config: { temperature?: number; max_tokens?: number; top_p?: number } = {};
|
||||
if (metadata?.temperature !== undefined) config.temperature = metadata.temperature;
|
||||
if (metadata?.max_tokens !== undefined) config.max_tokens = metadata.max_tokens;
|
||||
if (metadata?.top_p !== undefined) config.top_p = metadata.top_p;
|
||||
|
||||
// Parse the content to extract messages
|
||||
// Try to parse as role-prefixed format first, then fall back to simple content
|
||||
const parsedBody = parseDotpromptBody(content);
|
||||
|
||||
// Extract tools from metadata
|
||||
const tools: Tool[] = [];
|
||||
if (metadata?.tools && Array.isArray(metadata.tools)) {
|
||||
metadata.tools.forEach((tool: any) => {
|
||||
tools.push({
|
||||
name: tool?.function?.name || "Unnamed Tool",
|
||||
description: tool?.function?.description || "",
|
||||
json: JSON.stringify(tool, null, 2),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name: baseName,
|
||||
model,
|
||||
config,
|
||||
tools,
|
||||
developerMessage: parsedBody.developerMessage,
|
||||
messages:
|
||||
parsedBody.messages.length > 0
|
||||
? parsedBody.messages
|
||||
: [{ role: "user", content: content || "Enter task specifics. Use {{template_variables}} for dynamic inputs" }],
|
||||
};
|
||||
};
|
||||
|
||||
export const getVersionNumber = (promptId?: string): string => {
|
||||
if (!promptId) return "1";
|
||||
// Match version with dot (.v), underscore (_v), or hyphen (-v) separator
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue