💭 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:
JOJO 2026-07-15 03:34:48 +09:00 committed by GitHub
parent 1ca7e78f49
commit 02a5b985e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 367 additions and 0 deletions

View file

@ -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({

View file

@ -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({