diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 3d19f65ad6..cf70ad42dd 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -352,6 +352,9 @@ "com_endpoint_top_p": "Top P", "com_endpoint_use_active_assistant": "Use Active Assistant", "com_endpoint_use_responses_api": "Use Responses API", + "com_endpoint_stored_prompt_id": "Stored Prompt ID", + "com_endpoint_stored_prompt_id_placeholder": "pmpt_...", + "com_endpoint_openai_stored_prompt_id": "OpenAI Playground stored prompt ID (pmpt_...). Routes the conversation through a pre-configured prompt that carries its own system instructions and file-search vector store. Automatically enables the Responses API.", "com_endpoint_use_search_grounding": "Grounding with Google Search", "com_endpoint_verbosity": "Verbosity", "com_error_endpoint_models_not_loaded": "Models for {{0}} could not be loaded. Please refresh the page and try again.", diff --git a/packages/api/src/endpoints/openai/llm.spec.ts b/packages/api/src/endpoints/openai/llm.spec.ts index a78cc4b87d..f77892d742 100644 --- a/packages/api/src/endpoints/openai/llm.spec.ts +++ b/packages/api/src/endpoints/openai/llm.spec.ts @@ -737,6 +737,68 @@ describe('extractDefaultParams', () => { }); }); +describe('Stored Playground Prompt (stored_prompt_id)', () => { + it('should inject prompt.id into modelKwargs and force useResponsesApi when stored_prompt_id is set', () => { + const result = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + modelOptions: { + model: 'gpt-4o', + stored_prompt_id: 'pmpt_abc123', + } as Partial, + }); + + expect(result.llmConfig).toHaveProperty('useResponsesApi', true); + expect(result.llmConfig.modelKwargs).toEqual( + expect.objectContaining({ prompt: { id: 'pmpt_abc123' } }), + ); + }); + + it('should not set modelKwargs.prompt when stored_prompt_id is absent', () => { + const result = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + modelOptions: { + model: 'gpt-4o', + useResponsesApi: true, + } as Partial, + }); + + expect(result.llmConfig.modelKwargs?.prompt).toBeUndefined(); + }); + + it('should preserve a manually-set useResponsesApi=true alongside stored_prompt_id', () => { + const result = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + modelOptions: { + model: 'gpt-4o', + useResponsesApi: true, + stored_prompt_id: 'pmpt_xyz789', + } as Partial, + }); + + expect(result.llmConfig).toHaveProperty('useResponsesApi', true); + expect(result.llmConfig.modelKwargs).toEqual( + expect.objectContaining({ prompt: { id: 'pmpt_xyz789' } }), + ); + }); + + it('should not add stored_prompt_id itself to llmConfig or modelKwargs as a raw field', () => { + const result = getOpenAILLMConfig({ + apiKey: 'test-api-key', + streaming: true, + modelOptions: { + model: 'gpt-4o', + stored_prompt_id: 'pmpt_abc123', + } as Partial, + }); + + expect(result.llmConfig).not.toHaveProperty('stored_prompt_id'); + expect(result.llmConfig.modelKwargs?.stored_prompt_id).toBeUndefined(); + }); +}); + describe('applyDefaultParams', () => { it('should apply defaults only when field is undefined', () => { const target: Record = { diff --git a/packages/api/src/endpoints/openai/llm.ts b/packages/api/src/endpoints/openai/llm.ts index a89f6fce44..8e173c145a 100644 --- a/packages/api/src/endpoints/openai/llm.ts +++ b/packages/api/src/endpoints/openai/llm.ts @@ -53,6 +53,7 @@ export const knownOpenAIParams = new Set([ 'truncation', 'include', 'previous_response_id', + 'stored_prompt_id', // LangChain specific '__includeRawResponse', 'maxConcurrency', @@ -150,6 +151,7 @@ export function getOpenAILLMConfig({ reasoning_summary, verbosity, web_search, + stored_prompt_id, frequency_penalty, presence_penalty, ...modelOptions @@ -274,6 +276,20 @@ export function getOpenAILLMConfig({ tools.push({ type: 'web_search' }); } + if (stored_prompt_id) { + /** + * Forward a stored OpenAI Playground prompt by ID. + * The Responses API accepts `prompt: { id, variables? }` which causes OpenAI + * to hydrate the stored system instructions, file-search vector store, and + * any tool definitions configured in the Playground. + * We force `useResponsesApi` here because Chat Completions does not support + * this parameter. + */ + llmConfig.useResponsesApi = true; + modelKwargs.prompt = { id: stored_prompt_id }; + hasModelKwargs = true; + } + /** * Note: OpenAI reasoning models (o1/o3/gpt-5) do not support temperature and other sampling parameters * Exception: gpt-5-chat and versioned models like gpt-5.1 DO support these parameters diff --git a/packages/data-provider/src/parameterSettings.ts b/packages/data-provider/src/parameterSettings.ts index d0cfdf210f..2271c09aed 100644 --- a/packages/data-provider/src/parameterSettings.ts +++ b/packages/data-provider/src/parameterSettings.ts @@ -268,6 +268,20 @@ const openAIParams: Record = { showDefault: false, columnSpan: 2, }, + stored_prompt_id: { + key: 'stored_prompt_id', + label: 'com_endpoint_stored_prompt_id', + labelCode: true, + type: 'string', + default: '', + component: 'input', + placeholder: 'com_endpoint_stored_prompt_id_placeholder', + placeholderCode: true, + description: 'com_endpoint_openai_stored_prompt_id', + descriptionCode: true, + optionType: 'model', + columnSpan: 4, + }, web_search: { key: 'web_search', label: 'com_ui_web_search', @@ -765,6 +779,7 @@ const openAI: SettingsConfiguration = [ openAIParams.web_search, openAIParams.reasoning_effort, openAIParams.useResponsesApi, + openAIParams.stored_prompt_id, openAIParams.reasoning_summary, openAIParams.verbosity, openAIParams.disableStreaming, @@ -791,6 +806,7 @@ const openAICol2: SettingsConfiguration = [ openAIParams.reasoning_summary, openAIParams.verbosity, openAIParams.useResponsesApi, + openAIParams.stored_prompt_id, openAIParams.web_search, openAIParams.disableStreaming, librechat.fileTokenLimit, diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 084f74af86..bc144dfc27 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -770,6 +770,14 @@ export const tConversationSchema = z.object({ verbosity: eVerbositySchema.optional().nullable(), /* OpenAI: use Responses API */ useResponsesApi: z.boolean().optional(), + /** + * OpenAI Playground stored prompt ID (e.g. "pmpt_..."). + * When set, LibreChat forwards `prompt: { id }` in the Responses API request, + * routing the call through a pre-configured Playground prompt that can carry + * its own system instructions, file-search vector store, and tool definitions. + * Implicitly enables `useResponsesApi`. + */ + stored_prompt_id: z.string().optional(), /* Anthropic: Effort control */ effort: eAnthropicEffortSchema.optional().nullable(), /* OpenAI Responses API / Anthropic API / Google API */ @@ -880,6 +888,8 @@ export const tQueryParamsSchema = tConversationSchema verbosity: true, /** @endpoints openAI, custom, azureOpenAI */ useResponsesApi: true, + /** @endpoints openAI, custom, azureOpenAI */ + stored_prompt_id: true, /** @endpoints openAI, anthropic, google */ web_search: true, /** @endpoints openAI, custom, azureOpenAI */ @@ -1178,6 +1188,7 @@ export const openAIBaseSchema = tConversationSchema.pick({ reasoning_summary: true, verbosity: true, useResponsesApi: true, + stored_prompt_id: true, web_search: true, disableStreaming: true, fileTokenLimit: true, diff --git a/packages/data-schemas/src/schema/defaults.ts b/packages/data-schemas/src/schema/defaults.ts index 9b50bceb1d..05d4b6f4c6 100644 --- a/packages/data-schemas/src/schema/defaults.ts +++ b/packages/data-schemas/src/schema/defaults.ts @@ -139,6 +139,10 @@ export const conversationPreset = { useResponsesApi: { type: Boolean, }, + /** OpenAI Playground stored prompt ID (pmpt_...) */ + stored_prompt_id: { + type: String, + }, /** OpenAI Responses API / Anthropic API / Google API */ web_search: { type: Boolean, diff --git a/packages/data-schemas/src/schema/preset.ts b/packages/data-schemas/src/schema/preset.ts index 5af5163fd3..7f8d7a16fc 100644 --- a/packages/data-schemas/src/schema/preset.ts +++ b/packages/data-schemas/src/schema/preset.ts @@ -50,6 +50,7 @@ export interface IPreset extends Document { reasoning_summary?: string; verbosity?: string; useResponsesApi?: boolean; + stored_prompt_id?: string; web_search?: boolean; disableStreaming?: boolean; fileTokenLimit?: number; diff --git a/packages/data-schemas/src/types/convo.ts b/packages/data-schemas/src/types/convo.ts index c7888efba2..be07751105 100644 --- a/packages/data-schemas/src/types/convo.ts +++ b/packages/data-schemas/src/types/convo.ts @@ -48,6 +48,7 @@ export interface IConversation extends Document { reasoning_summary?: string; verbosity?: string; useResponsesApi?: boolean; + stored_prompt_id?: string; web_search?: boolean; disableStreaming?: boolean; fileTokenLimit?: number;