🧠 feat: Add GPT-5.6 reasoning.mode + reasoning.context (Responses API) (#14233)

Follow-up to #14206 (issue #14203 items 2-4). Adds two OpenAI Responses API
reasoning parameters that ride inside the `reasoning` object:

- reasoning_mode: standard | pro
- reasoning_context: auto | current_turn | all_turns

Wired end-to-end mirroring reasoning_summary: zod schema + query/base picks,
UI SettingDefinitions (openAI + openAICol2), data-schemas types, i18n, and
the backend (hasReasoningParams/getReasoningObject/applyReasoningConfig +
getOpenAILLMConfig threading + dropParams cleanup via removeReasoningField).
Responses-API-only: they flow into llmConfig.reasoning (OpenAI) or
modelKwargs.reasoning (custom useResponsesApi), and are excluded from
Chat Completions and OpenRouter.

Per-model gating (hiding pro/max where unsupported) and persisted reasoning
(#14203 item 5) remain separate follow-ups.
This commit is contained in:
Danny Avila 2026-07-13 09:52:42 -04:00 • committed by GitHub
parent 11caf5f21c
commit b3f9cddbef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 293 additions and 6 deletions

View file

@ -310,7 +310,9 @@
"com_endpoint_openai_max_tokens": "Optional 'max_tokens' field, representing the maximum number of tokens that can be generated in the chat completion. The total length of input tokens and generated tokens is limited by the models context length. You may experience errors if this number exceeds the max context tokens.",
"com_endpoint_openai_pres": "Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.",
"com_endpoint_openai_prompt_prefix_placeholder": "Set custom instructions to include in System Message. Default: none",
"com_endpoint_openai_reasoning_context": "Responses API only: controls how much prior conversation reasoning the model reuses. Set to auto, current_turn, or all_turns.",
"com_endpoint_openai_reasoning_effort": "Reasoning models only: constrains effort on reasoning. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response. 'Minimal' produces very few reasoning tokens for fastest time-to-first-token, especially well-suited for coding and instruction following.",
"com_endpoint_openai_reasoning_mode": "Responses API only: selects the reasoning mode. 'Pro' enables extended reasoning on supported GPT-5.6 models. Set to standard or pro.",
"com_endpoint_openai_reasoning_summary": "Responses API only: A summary of the reasoning performed by the model. This can be useful for debugging and understanding the model's reasoning process. Set to none,auto, concise, or detailed.",
"com_endpoint_openai_resend": "Resend all previously attached images. Note: this can significantly increase token cost and you may experience errors with many image attachments.",
"com_endpoint_openai_resend_files": "Resend all previously attached files. Note: this will increase token cost and you may experience errors with many attachments.",
@ -349,7 +351,9 @@
"com_endpoint_prompt_prefix_assistants": "Additional Instructions",
"com_endpoint_prompt_prefix_assistants_placeholder": "Set additional instructions or context on top of the Assistant's main instructions. Ignored if empty.",
"com_endpoint_prompt_prefix_placeholder": "Set custom instructions or context. Ignored if empty.",
"com_endpoint_reasoning_context": "Reasoning Context",
"com_endpoint_reasoning_effort": "Reasoning Effort",
"com_endpoint_reasoning_mode": "Reasoning Mode",
"com_endpoint_reasoning_summary": "Reasoning Summary",
"com_endpoint_save_as_preset": "Save As Preset",
"com_endpoint_search": "Search endpoint by name",
@ -811,6 +815,7 @@
"com_ui_all": "all",
"com_ui_all_projects": "All projects",
"com_ui_all_proper": "All",
"com_ui_all_turns": "All turns",
"com_ui_analyzing": "Analyzing",
"com_ui_analyzing_finished": "Finished analyzing",
"com_ui_api_key": "API Key",
@ -1050,6 +1055,7 @@
"com_ui_creating_file": "Creating {{0}}",
"com_ui_creating_image": "Creating image. May take a moment",
"com_ui_current": "Current",
"com_ui_current_turn": "Current turn",
"com_ui_currently_production": "Currently in production",
"com_ui_custom": "Custom",
"com_ui_custom_header_name": "Custom Header Name",
@ -1504,6 +1510,7 @@
"com_ui_preview_unavailable": "Preview not available for this file type",
"com_ui_privacy_policy": "Privacy policy",
"com_ui_privacy_policy_url": "Privacy Policy URL",
"com_ui_pro": "Pro",
"com_ui_production": "Production",
"com_ui_project_chat_count": "{{count}} chats",
"com_ui_project_chat_count_single": "1 chat",
@ -1838,6 +1845,7 @@
"com_ui_sr_global_prompt": "Global prompt group",
"com_ui_sr_public_skill": "Public skill",
"com_ui_stack_trace": "Stack Trace",
"com_ui_standard": "Standard",
"com_ui_stateful_sessions": "Stateful code sessions",
"com_ui_status_prefix": "Status:",
"com_ui_stop": "Stop",

View file

@ -1,7 +1,9 @@
import {
Verbosity,
EModelEndpoint,
ReasoningMode,
ReasoningEffort,
ReasoningContext,
ReasoningSummary,
ReasoningParameterFormat,
} from 'librechat-data-provider';
@ -501,6 +503,29 @@ describe('getOpenAILLMConfig', () => {
expect(result.llmConfig).not.toHaveProperty('reasoning_effort');
});
it('should include reasoning_mode and reasoning_context in the custom reasoning object', () => {
const result = getOpenAILLMConfig({
apiKey: 'test-api-key',
streaming: true,
endpoint: 'custom',
reasoningFormat: ReasoningParameterFormat.reasoningObject,
modelOptions: {
model: 'provider/gpt-5.6',
reasoning_effort: ReasoningEffort.high,
reasoning_mode: ReasoningMode.pro,
reasoning_context: ReasoningContext.all_turns,
},
});
expect(result.llmConfig.modelKwargs).toHaveProperty('reasoning', {
effort: ReasoningEffort.high,
mode: ReasoningMode.pro,
context: ReasoningContext.all_turns,
});
expect(result.llmConfig).not.toHaveProperty('reasoning_mode');
expect(result.llmConfig).not.toHaveProperty('reasoning_context');
});
it('should apply reasoning format to default reasoning params', () => {
const result = getOpenAILLMConfig({
apiKey: 'test-api-key',
@ -640,6 +665,46 @@ describe('getOpenAILLMConfig', () => {
summary: ReasoningSummary.detailed,
});
});
it('should build the OpenAI Responses reasoning object from mode and context alone', () => {
const result = getOpenAILLMConfig({
apiKey: 'test-api-key',
streaming: true,
endpoint: EModelEndpoint.openAI,
modelOptions: {
model: 'gpt-5.6',
reasoning_mode: ReasoningMode.pro,
reasoning_context: ReasoningContext.current_turn,
useResponsesApi: true,
},
});
expect(result.llmConfig.reasoning).toEqual({
mode: ReasoningMode.pro,
context: ReasoningContext.current_turn,
});
});
it('should omit reasoning_mode and reasoning_context on OpenAI Chat Completions', () => {
const result = getOpenAILLMConfig({
apiKey: 'test-api-key',
streaming: true,
endpoint: EModelEndpoint.openAI,
modelOptions: {
model: 'gpt-5.6',
reasoning_effort: ReasoningEffort.high,
reasoning_mode: ReasoningMode.pro,
reasoning_context: ReasoningContext.all_turns,
},
});
/** Chat Completions uses reasoning_effort; mode/context are Responses-only
* and must never leak as top-level params or a reasoning object. */
expect(result.llmConfig).not.toHaveProperty('reasoning');
expect(result.llmConfig).toHaveProperty('reasoning_effort', ReasoningEffort.high);
expect(result.llmConfig).not.toHaveProperty('reasoning_mode');
expect(result.llmConfig).not.toHaveProperty('reasoning_context');
});
});
describe('Default and Add Parameters', () => {
@ -1022,6 +1087,24 @@ describe('getOpenAILLMConfig', () => {
});
});
it('should exclude reasoning_mode and reasoning_context from OpenRouter reasoning object', () => {
const result = getOpenAILLMConfig({
apiKey: 'test-api-key',
streaming: true,
useOpenRouter: true,
modelOptions: {
model: 'anthropic/claude-3-sonnet',
reasoning_effort: ReasoningEffort.high,
reasoning_mode: ReasoningMode.pro,
reasoning_context: ReasoningContext.all_turns,
},
});
expect(result.llmConfig.modelKwargs).toHaveProperty('reasoning', {
effort: ReasoningEffort.high,
});
});
it.each([ReasoningEffort.xhigh, ReasoningEffort.minimal, ReasoningEffort.none])(
'should support OpenRouter effort level: %s',
(effort) => {

View file

@ -77,27 +77,39 @@ export const knownOpenAIParams: Set<string> = new Set([
function hasReasoningParams({
reasoning_effort,
reasoning_summary,
reasoning_mode,
reasoning_context,
}: {
reasoning_effort?: string | null;
reasoning_summary?: string | null;
reasoning_mode?: string | null;
reasoning_context?: string | null;
}): boolean {
return (
(reasoning_effort != null && reasoning_effort !== '') ||
(reasoning_summary != null && reasoning_summary !== '')
(reasoning_summary != null && reasoning_summary !== '') ||
(reasoning_mode != null && reasoning_mode !== '') ||
(reasoning_context != null && reasoning_context !== '')
);
}
function getReasoningObject({
reasoningEffort,
reasoningSummary,
reasoningMode,
reasoningContext,
}: {
reasoningEffort?: OpenAILLMConfig['reasoning_effort'];
reasoningSummary?: OpenAILLMConfig['reasoning_summary'];
reasoningMode?: OpenAILLMConfig['reasoning_mode'];
reasoningContext?: OpenAILLMConfig['reasoning_context'];
}): OpenAI.Reasoning {
return removeNullishValues(
{
effort: reasoningEffort,
summary: reasoningSummary,
mode: reasoningMode,
context: reasoningContext,
},
true,
) as OpenAI.Reasoning;
@ -107,14 +119,14 @@ function isOpenAIEndpoint(endpoint?: EModelEndpoint | string | null): boolean {
return endpoint === EModelEndpoint.openAI || endpoint === EModelEndpoint.azureOpenAI;
}
function removeReasoningSummary(target: Record<string, unknown>) {
function removeReasoningField(target: Record<string, unknown>, field: string) {
const { reasoning } = target;
if (reasoning == null || typeof reasoning !== 'object' || Array.isArray(reasoning)) {
return;
}
const rest = { ...(reasoning as Record<string, unknown>) };
delete rest.summary;
delete rest[field];
if (Object.keys(rest).length === 0) {
delete target.reasoning;
return;
@ -146,8 +158,24 @@ function deleteConfigParam({
if (param === 'reasoning_summary') {
delete (llmConfig as Record<string, unknown>).reasoning_summary;
delete modelKwargs.reasoning_summary;
removeReasoningSummary(llmConfig as Record<string, unknown>);
removeReasoningSummary(modelKwargs);
removeReasoningField(llmConfig as Record<string, unknown>, 'summary');
removeReasoningField(modelKwargs, 'summary');
return;
}
if (param === 'reasoning_mode') {
delete (llmConfig as Record<string, unknown>).reasoning_mode;
delete modelKwargs.reasoning_mode;
removeReasoningField(llmConfig as Record<string, unknown>, 'mode');
removeReasoningField(modelKwargs, 'mode');
return;
}
if (param === 'reasoning_context') {
delete (llmConfig as Record<string, unknown>).reasoning_context;
delete modelKwargs.reasoning_context;
removeReasoningField(llmConfig as Record<string, unknown>, 'context');
removeReasoningField(modelKwargs, 'context');
return;
}
@ -285,6 +313,8 @@ function applyReasoningConfig({
reasoningEffort,
reasoningFormat,
reasoningSummary,
reasoningMode,
reasoningContext,
}: {
endpoint?: EModelEndpoint | string | null;
llmConfig: OpenAILLMConfig;
@ -292,17 +322,26 @@ function applyReasoningConfig({
reasoningEffort?: OpenAILLMConfig['reasoning_effort'];
reasoningFormat?: ReasoningParameterFormat;
reasoningSummary?: OpenAILLMConfig['reasoning_summary'];
reasoningMode?: OpenAILLMConfig['reasoning_mode'];
reasoningContext?: OpenAILLMConfig['reasoning_context'];
}): boolean {
if (
!hasReasoningParams({
reasoning_effort: reasoningEffort,
reasoning_summary: reasoningSummary,
reasoning_mode: reasoningMode,
reasoning_context: reasoningContext,
})
) {
return false;
}
const reasoning = getReasoningObject({ reasoningEffort, reasoningSummary });
const reasoning = getReasoningObject({
reasoningEffort,
reasoningSummary,
reasoningMode,
reasoningContext,
});
if (reasoningFormat === ReasoningParameterFormat.disabled) {
return false;
}
@ -452,6 +491,8 @@ export function getOpenAILLMConfig({
const {
reasoning_effort,
reasoning_summary,
reasoning_mode,
reasoning_context,
verbosity,
web_search,
promptCache,
@ -482,6 +523,8 @@ export function getOpenAILLMConfig({
let hasModelKwargs = false;
let reasoningEffort = reasoning_effort;
let reasoningSummary = reasoning_summary;
let reasoningMode = reasoning_mode;
let reasoningContext = reasoning_context;
if (verbosity != null && verbosity !== '' && useOpenRouter) {
llmConfig.verbosity = verbosity;
@ -527,6 +570,18 @@ export function getOpenAILLMConfig({
}
continue;
}
if (key === 'reasoning_mode') {
if (!reasoningMode && typeof value === 'string') {
reasoningMode = value as OpenAILLMConfig['reasoning_mode'];
}
continue;
}
if (key === 'reasoning_context') {
if (!reasoningContext && typeof value === 'string') {
reasoningContext = value as OpenAILLMConfig['reasoning_context'];
}
continue;
}
if (key === 'verbosity') {
hasModelKwargs =
applyVerbosityParam({
@ -584,6 +639,18 @@ export function getOpenAILLMConfig({
}
continue;
}
if (key === 'reasoning_mode') {
if (typeof value === 'string' || value == null) {
reasoningMode = value as OpenAILLMConfig['reasoning_mode'];
}
continue;
}
if (key === 'reasoning_context') {
if (typeof value === 'string' || value == null) {
reasoningContext = value as OpenAILLMConfig['reasoning_context'];
}
continue;
}
if (key === 'verbosity') {
hasModelKwargs =
applyVerbosityParam({
@ -664,6 +731,8 @@ export function getOpenAILLMConfig({
reasoningFormat,
reasoningEffort,
reasoningSummary,
reasoningMode,
reasoningContext,
}) || hasModelKwargs;
}

View file

@ -10,6 +10,8 @@ import {
ReasoningEffort,
AnthropicEffort,
ReasoningSummary,
ReasoningMode,
ReasoningContext,
BedrockProviders,
anthropicSettings,
} from './types';
@ -309,6 +311,48 @@ const openAIParams: Record<string, SettingDefinition> = {
optionType: 'model',
columnSpan: 4,
},
reasoning_mode: {
key: 'reasoning_mode',
label: 'com_endpoint_reasoning_mode',
labelCode: true,
description: 'com_endpoint_openai_reasoning_mode',
descriptionCode: true,
type: 'enum',
default: ReasoningMode.unset,
component: 'slider',
options: [ReasoningMode.unset, ReasoningMode.standard, ReasoningMode.pro],
enumMappings: {
[ReasoningMode.unset]: 'com_ui_unset',
[ReasoningMode.standard]: 'com_ui_standard',
[ReasoningMode.pro]: 'com_ui_pro',
},
optionType: 'model',
columnSpan: 4,
},
reasoning_context: {
key: 'reasoning_context',
label: 'com_endpoint_reasoning_context',
labelCode: true,
description: 'com_endpoint_openai_reasoning_context',
descriptionCode: true,
type: 'enum',
default: ReasoningContext.unset,
component: 'slider',
options: [
ReasoningContext.unset,
ReasoningContext.auto,
ReasoningContext.current_turn,
ReasoningContext.all_turns,
],
enumMappings: {
[ReasoningContext.unset]: 'com_ui_unset',
[ReasoningContext.auto]: 'com_ui_auto',
[ReasoningContext.current_turn]: 'com_ui_current_turn',
[ReasoningContext.all_turns]: 'com_ui_all_turns',
},
optionType: 'model',
columnSpan: 4,
},
verbosity: {
key: 'verbosity',
label: 'com_endpoint_verbosity',
@ -836,6 +880,8 @@ const openAI: SettingsConfiguration = [
openAIParams.reasoning_effort,
openAIParams.useResponsesApi,
openAIParams.reasoning_summary,
openAIParams.reasoning_mode,
openAIParams.reasoning_context,
openAIParams.verbosity,
openAIParams.disableStreaming,
librechat.fileTokenLimit,
@ -865,6 +911,8 @@ const openAICol2: SettingsConfiguration = [
baseDefinitions.imageDetail,
openAIParams.reasoning_effort,
openAIParams.reasoning_summary,
openAIParams.reasoning_mode,
openAIParams.reasoning_context,
openAIParams.verbosity,
openAIParams.useResponsesApi,
openAIParams.web_search,

View file

@ -1,11 +1,15 @@
import {
AnthropicEffort,
ReasoningEffort,
ReasoningMode,
ReasoningContext,
googleSettings,
anthropicSettings,
compactGoogleSchema,
eAnthropicEffortSchema,
eReasoningEffortSchema,
eReasoningModeSchema,
eReasoningContextSchema,
} from './schemas';
describe('anthropicSettings', () => {
@ -586,3 +590,34 @@ describe('ReasoningEffort', () => {
expect(() => eReasoningEffortSchema.parse('ultra')).toThrow();
});
});
describe('ReasoningMode', () => {
it('exposes standard and pro plus an unset sentinel', () => {
expect(ReasoningMode.unset).toBe('');
expect(ReasoningMode.standard).toBe('standard');
expect(ReasoningMode.pro).toBe('pro');
});
it('accepts its values through the zod schema and rejects unknowns', () => {
expect(eReasoningModeSchema.parse('standard')).toBe('standard');
expect(eReasoningModeSchema.parse('pro')).toBe('pro');
expect(eReasoningModeSchema.parse('')).toBe('');
expect(() => eReasoningModeSchema.parse('turbo')).toThrow();
});
});
describe('ReasoningContext', () => {
it('exposes auto, current_turn, and all_turns plus an unset sentinel', () => {
expect(ReasoningContext.unset).toBe('');
expect(ReasoningContext.auto).toBe('auto');
expect(ReasoningContext.current_turn).toBe('current_turn');
expect(ReasoningContext.all_turns).toBe('all_turns');
});
it('accepts its values through the zod schema and rejects unknowns', () => {
expect(eReasoningContextSchema.parse('auto')).toBe('auto');
expect(eReasoningContextSchema.parse('current_turn')).toBe('current_turn');
expect(eReasoningContextSchema.parse('all_turns')).toBe('all_turns');
expect(() => eReasoningContextSchema.parse('next_turn')).toThrow();
});
});

View file

@ -275,6 +275,21 @@ export enum ThinkingLevel {
high = 'high',
}
/** OpenAI Responses API `reasoning.mode` (GPT-5.6+). */
export enum ReasoningMode {
unset = '',
standard = 'standard',
pro = 'pro',
}
/** OpenAI Responses API `reasoning.context` (GPT-5.6+). */
export enum ReasoningContext {
unset = '',
auto = 'auto',
current_turn = 'current_turn',
all_turns = 'all_turns',
}
export const imageDetailNumeric = {
[ImageDetail.low]: 0,
[ImageDetail.auto]: 1,
@ -296,6 +311,8 @@ export const eThinkingDisplaySchema = z.nativeEnum(ThinkingDisplay);
export const eReasoningSummarySchema = z.nativeEnum(ReasoningSummary);
export const eVerbositySchema = z.nativeEnum(Verbosity);
export const eThinkingLevelSchema = z.nativeEnum(ThinkingLevel);
export const eReasoningModeSchema = z.nativeEnum(ReasoningMode);
export const eReasoningContextSchema = z.nativeEnum(ReasoningContext);
export const defaultAssistantFormValues = {
assistant: '',
@ -973,6 +990,9 @@ export const tConversationSchema = z.object({
/* OpenAI: Reasoning models only */
reasoning_effort: eReasoningEffortSchema.optional().nullable(),
reasoning_summary: eReasoningSummarySchema.optional().nullable(),
/* OpenAI Responses API: reasoning mode (standard/pro) + context */
reasoning_mode: eReasoningModeSchema.optional().nullable(),
reasoning_context: eReasoningContextSchema.optional().nullable(),
/* OpenAI: Verbosity control */
verbosity: eVerbositySchema.optional().nullable(),
/* OpenAI: use Responses API */
@ -1090,6 +1110,10 @@ export const tQueryParamsSchema = tConversationSchema
/** @endpoints openAI, custom, azureOpenAI */
reasoning_summary: true,
/** @endpoints openAI, custom, azureOpenAI */
reasoning_mode: true,
/** @endpoints openAI, custom, azureOpenAI */
reasoning_context: true,
/** @endpoints openAI, custom, azureOpenAI */
verbosity: true,
/** @endpoints openAI, custom, azureOpenAI */
useResponsesApi: true,
@ -1404,6 +1428,8 @@ export const openAIBaseSchema = tConversationSchema.pick({
max_tokens: true,
reasoning_effort: true,
reasoning_summary: true,
reasoning_mode: true,
reasoning_context: true,
verbosity: true,
useResponsesApi: true,
web_search: true,

View file

@ -183,6 +183,13 @@ export const conversationPreset: {
reasoning_summary: {
type: StringConstructor;
};
/** Responses API reasoning mode (standard/pro) + context */
reasoning_mode: {
type: StringConstructor;
};
reasoning_context: {
type: StringConstructor;
};
/** Verbosity control */
verbosity: {
type: StringConstructor;
@ -349,6 +356,13 @@ export const conversationPreset: {
reasoning_summary: {
type: String,
},
/** Responses API reasoning mode (standard/pro) + context */
reasoning_mode: {
type: String,
},
reasoning_context: {
type: String,
},
/** Verbosity control */
verbosity: {
type: String,

View file

@ -49,6 +49,8 @@ export interface IPreset extends Document {
max_tokens?: number;
reasoning_effort?: string;
reasoning_summary?: string;
reasoning_mode?: string;
reasoning_context?: string;
verbosity?: string;
useResponsesApi?: boolean;
web_search?: boolean;

View file

@ -50,6 +50,8 @@ export interface IConversation extends Document {
max_tokens?: number;
reasoning_effort?: string;
reasoning_summary?: string;
reasoning_mode?: string;
reasoning_context?: string;
verbosity?: string;
useResponsesApi?: boolean;
web_search?: boolean;