mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 14:57:42 +00:00
💭 fix: Default GPT-5.6 Reasoning Requests to Responses API (#14232)
* 💭 fix: Default GPT-5.6 Reasoning Requests to Responses API GPT-5.6 models reject function tools combined with `reasoning_effort` in /v1/chat/completions (400: "Function tools with reasoning_effort are not supported ... To use function tools, use /v1/responses or set reasoning_effort to 'none'"). Default `useResponsesApi` to `true` for GPT-5.6 models on the OpenAI endpoint when a reasoning effort other than 'none' is requested, unless the user explicitly set `useResponsesApi`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 💭 fix: Address GPT-5.6 Responses API guard review feedback - Skip the auto-switch for OpenRouter-backed OpenAI endpoint configs; OpenRouter keeps its own reasoning path - Respect `dropParams: ['useResponsesApi']` as an explicit opt-out so Chat Completions requests never carry a nested reasoning payload - Stop treating `dropParams: ['reasoning']` as dropping reasoning: `deleteConfigParam` only removes the nested object, so skipping the switch there would resend flat `reasoning_effort` and re-trigger the 400 - Reword docstring: reasoning without tools still works on Chat Completions; the default exists because tools bind after config time - Reconcile with #14233: the Chat Completions mode/context omission test now opts out explicitly, and a new test locks in that the Responses API default carries reasoning mode/context in the nested reasoning object Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * 💭 fix: Scope GPT-5.6 Responses API default to first-party OpenAI - Skip the auto-switch for custom gateway base URLs (reverseProxyUrl/ directEndpoint may expose only /v1/chat/completions); the Responses default now applies to canonical api.openai.com only - Skip when reasoningFormat is 'disabled': no reasoning payload is sent, so there is no tool incompatibility to avoid and the request stays on the configured Chat Completions path - Add coverage for custom gateway base URL, canonical base URL, and disabled reasoning format --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
1ca7e78f49
commit
02a5b985e4
2 changed files with 367 additions and 0 deletions
|
|
@ -695,6 +695,8 @@ describe('getOpenAILLMConfig', () => {
|
|||
reasoning_effort: ReasoningEffort.high,
|
||||
reasoning_mode: ReasoningMode.pro,
|
||||
reasoning_context: ReasoningContext.all_turns,
|
||||
/** Explicit opt-out: GPT-5.6 reasoning otherwise defaults to the Responses API */
|
||||
useResponsesApi: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -707,6 +709,304 @@ describe('getOpenAILLMConfig', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('GPT-5.6 Responses API Requirement', () => {
|
||||
it.each(['gpt-5.6-terra', 'gpt-5.6-luna', 'gpt-5.6-sol', 'gpt-5.6'])(
|
||||
'should default to Responses API for %s when reasoning_effort is set',
|
||||
(model) => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
modelOptions: {
|
||||
model,
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).toHaveProperty('useResponsesApi', true);
|
||||
expect(result.llmConfig.reasoning).toEqual({ effort: ReasoningEffort.high });
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning_effort');
|
||||
},
|
||||
);
|
||||
|
||||
it('should NOT default to Responses API without reasoning params', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
modelOptions: {
|
||||
model: 'gpt-5.6-terra',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).not.toHaveProperty('useResponsesApi');
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning');
|
||||
});
|
||||
|
||||
it('should NOT default to Responses API when reasoning_effort is none', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
modelOptions: {
|
||||
model: 'gpt-5.6-terra',
|
||||
reasoning_effort: ReasoningEffort.none,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).not.toHaveProperty('useResponsesApi');
|
||||
expect(result.llmConfig).toHaveProperty('reasoning_effort', ReasoningEffort.none);
|
||||
});
|
||||
|
||||
it('should respect an explicit useResponsesApi: false', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
modelOptions: {
|
||||
model: 'gpt-5.6-terra',
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
useResponsesApi: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).toHaveProperty('useResponsesApi', false);
|
||||
expect(result.llmConfig).toHaveProperty('reasoning_effort', ReasoningEffort.high);
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning');
|
||||
});
|
||||
|
||||
it.each(['gpt-5', 'gpt-5-pro', 'gpt-5.4-nano', 'gpt-5.5-preview', 'gpt-5-chat', 'o3-mini'])(
|
||||
'should NOT default to Responses API for %s',
|
||||
(model) => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
modelOptions: {
|
||||
model,
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).not.toHaveProperty('useResponsesApi');
|
||||
expect(result.llmConfig).toHaveProperty('reasoning_effort', ReasoningEffort.high);
|
||||
},
|
||||
);
|
||||
|
||||
it('should NOT default to Responses API for non-OpenAI endpoints', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: 'custom',
|
||||
modelOptions: {
|
||||
model: 'gpt-5.6-terra',
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).not.toHaveProperty('useResponsesApi');
|
||||
expect(result.llmConfig.modelKwargs).toHaveProperty('reasoning_effort', ReasoningEffort.high);
|
||||
});
|
||||
|
||||
it('should default to Responses API when reasoning_effort comes from defaultParams', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
defaultParams: {
|
||||
reasoning_effort: ReasoningEffort.medium,
|
||||
},
|
||||
modelOptions: {
|
||||
model: 'gpt-5.6-terra',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).toHaveProperty('useResponsesApi', true);
|
||||
expect(result.llmConfig.reasoning).toEqual({ effort: ReasoningEffort.medium });
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning_effort');
|
||||
});
|
||||
|
||||
it('should evaluate the final model when addParams overrides it to GPT-5.6', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
modelOptions: {
|
||||
model: 'gpt-4o',
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
addParams: {
|
||||
model: 'gpt-5.6-terra',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).toHaveProperty('model', 'gpt-5.6-terra');
|
||||
expect(result.llmConfig).toHaveProperty('useResponsesApi', true);
|
||||
expect(result.llmConfig.reasoning).toEqual({ effort: ReasoningEffort.high });
|
||||
});
|
||||
|
||||
it('should NOT default to Responses API when addParams overrides GPT-5.6 away', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
modelOptions: {
|
||||
model: 'gpt-5.6-terra',
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
addParams: {
|
||||
model: 'gpt-4.1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).toHaveProperty('model', 'gpt-4.1');
|
||||
expect(result.llmConfig).not.toHaveProperty('useResponsesApi');
|
||||
expect(result.llmConfig).toHaveProperty('reasoning_effort', ReasoningEffort.high);
|
||||
});
|
||||
|
||||
it('should NOT default to Responses API when dropParams removes reasoning_effort', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
modelOptions: {
|
||||
model: 'gpt-5.6-terra',
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
dropParams: ['reasoning_effort'],
|
||||
});
|
||||
|
||||
expect(result.llmConfig).not.toHaveProperty('useResponsesApi');
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning');
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning_effort');
|
||||
});
|
||||
|
||||
it('should still default to Responses API when dropParams removes only the reasoning object', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
modelOptions: {
|
||||
model: 'gpt-5.6-terra',
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
dropParams: ['reasoning'],
|
||||
});
|
||||
|
||||
expect(result.llmConfig).toHaveProperty('useResponsesApi', true);
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning');
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning_effort');
|
||||
});
|
||||
|
||||
it('should NOT default to Responses API when dropParams removes useResponsesApi', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
modelOptions: {
|
||||
model: 'gpt-5.6-terra',
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
dropParams: ['useResponsesApi'],
|
||||
});
|
||||
|
||||
expect(result.llmConfig).not.toHaveProperty('useResponsesApi');
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning');
|
||||
expect(result.llmConfig).toHaveProperty('reasoning_effort', ReasoningEffort.high);
|
||||
});
|
||||
|
||||
it('should NOT default to Responses API for OpenRouter-backed OpenAI endpoints', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
useOpenRouter: true,
|
||||
modelOptions: {
|
||||
model: 'gpt-5.6-terra',
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).not.toHaveProperty('useResponsesApi');
|
||||
expect(result.llmConfig.modelKwargs).toHaveProperty('reasoning', {
|
||||
effort: ReasoningEffort.high,
|
||||
});
|
||||
});
|
||||
|
||||
it('should carry reasoning_mode and reasoning_context when defaulting to Responses API', () => {
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).toHaveProperty('useResponsesApi', true);
|
||||
expect(result.llmConfig.reasoning).toEqual({
|
||||
effort: ReasoningEffort.high,
|
||||
mode: ReasoningMode.pro,
|
||||
context: ReasoningContext.all_turns,
|
||||
});
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning_effort');
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning_mode');
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning_context');
|
||||
});
|
||||
|
||||
it('should NOT default to Responses API for a custom gateway base URL', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
baseURL: 'https://gateway.example.com/v1',
|
||||
modelOptions: {
|
||||
model: 'gpt-5.6-terra',
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).not.toHaveProperty('useResponsesApi');
|
||||
expect(result.llmConfig).toHaveProperty('reasoning_effort', ReasoningEffort.high);
|
||||
});
|
||||
|
||||
it('should default to Responses API for the canonical OpenAI base URL', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
baseURL: 'https://api.openai.com/v1',
|
||||
modelOptions: {
|
||||
model: 'gpt-5.6-terra',
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).toHaveProperty('useResponsesApi', true);
|
||||
expect(result.llmConfig.reasoning).toEqual({ effort: ReasoningEffort.high });
|
||||
});
|
||||
|
||||
it('should NOT default to Responses API when reasoningFormat is disabled', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
apiKey: 'test-api-key',
|
||||
streaming: true,
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
reasoningFormat: ReasoningParameterFormat.disabled,
|
||||
modelOptions: {
|
||||
model: 'gpt-5.6-terra',
|
||||
reasoning_effort: ReasoningEffort.high,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.llmConfig).not.toHaveProperty('useResponsesApi');
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning');
|
||||
expect(result.llmConfig).not.toHaveProperty('reasoning_effort');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Default and Add Parameters', () => {
|
||||
it('should apply default parameters when fields are undefined', () => {
|
||||
const result = getOpenAILLMConfig({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
EModelEndpoint,
|
||||
ReasoningEffort,
|
||||
ReasoningParameterFormat,
|
||||
removeNullishValues,
|
||||
supportsAdaptiveThinking,
|
||||
|
|
@ -119,6 +120,48 @@ function isOpenAIEndpoint(endpoint?: EModelEndpoint | string | null): boolean {
|
|||
return endpoint === EModelEndpoint.openAI || endpoint === EModelEndpoint.azureOpenAI;
|
||||
}
|
||||
|
||||
/**
|
||||
* GPT-5.6 models reject function tools combined with `reasoning_effort` in
|
||||
* `/v1/chat/completions` (400: "To use function tools, use /v1/responses or
|
||||
* set reasoning_effort to 'none'"). Reasoning without tools still works on
|
||||
* Chat Completions, but tools are bound after config time, so GPT-5.6
|
||||
* reasoning requests default to the Responses API to avoid tool failures.
|
||||
*/
|
||||
const responsesApiRequiredPattern = /\bgpt-5\.6\b/;
|
||||
|
||||
function requiresResponsesApiForReasoning({
|
||||
model,
|
||||
reasoningEffort,
|
||||
}: {
|
||||
model?: string;
|
||||
reasoningEffort?: string | null;
|
||||
}): boolean {
|
||||
if (typeof model !== 'string' || !responsesApiRequiredPattern.test(model)) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
reasoningEffort != null &&
|
||||
reasoningEffort !== ReasoningEffort.unset &&
|
||||
reasoningEffort !== ReasoningEffort.none
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The GPT-5.6 Responses API default is first-party OpenAI only. A
|
||||
* `reverseProxyUrl`/`directEndpoint` gateway sets a custom base URL and may
|
||||
* expose only `/v1/chat/completions`, so it keeps its configured path.
|
||||
*/
|
||||
function isCanonicalOpenAIBaseURL(baseURL?: string | null): boolean {
|
||||
if (!baseURL) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return /(^|\.)api\.openai\.com$/i.test(new URL(baseURL).hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function removeReasoningField(target: Record<string, unknown>, field: string) {
|
||||
const { reasoning } = target;
|
||||
if (reasoning == null || typeof reasoning !== 'object' || Array.isArray(reasoning)) {
|
||||
|
|
@ -722,6 +765,30 @@ export function getOpenAILLMConfig({
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default GPT-5.6 reasoning requests to the Responses API unless explicitly set.
|
||||
* Reads `llmConfig.model` (reflects `addParams` overrides) and skips when
|
||||
* `dropParams` removes `reasoning_effort` later anyway (`'reasoning'` only
|
||||
* drops the nested object, not the flat param) or opts out of the Responses
|
||||
* API entirely. Limited to first-party OpenAI: OpenRouter, custom gateways
|
||||
* (non-canonical base URL), and `reasoningFormat: 'disabled'` (no reasoning
|
||||
* payload is sent) keep their existing Chat Completions path.
|
||||
*/
|
||||
const responsesApiOptedOut =
|
||||
dropParams != null &&
|
||||
(dropParams.includes('reasoning_effort') || dropParams.includes('useResponsesApi'));
|
||||
if (
|
||||
!useOpenRouter &&
|
||||
endpoint === EModelEndpoint.openAI &&
|
||||
isCanonicalOpenAIBaseURL(baseURL) &&
|
||||
reasoningFormat !== ReasoningParameterFormat.disabled &&
|
||||
llmConfig.useResponsesApi == null &&
|
||||
!responsesApiOptedOut &&
|
||||
requiresResponsesApiForReasoning({ model: llmConfig.model, reasoningEffort })
|
||||
) {
|
||||
llmConfig.useResponsesApi = true;
|
||||
}
|
||||
|
||||
if (!useOpenRouter) {
|
||||
hasModelKwargs =
|
||||
applyReasoningConfig({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue