fix(core): Pass reasoning effort on open router model node (#41179)

This commit is contained in:
Anne Aguirre 2026-10-16 12:43:58 +00:00 committed by GitHub
parent 1b02863c00
commit 683fd11259
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 129 additions and 22 deletions

View File

@ -199,6 +199,36 @@ export class LmChatOpenRouter implements INodeType {
type: 'collection',
default: {},
options: [
{
displayName: 'Reasoning Effort',
name: 'reasoningEffort',
type: 'options',
default: 'medium',
description:
'Controls the amount of reasoning effort the model will use to generate a response. Higher values may result in more accurate and detailed responses, but may also take longer to generate.',
options: [
{
name: 'Extra High',
value: 'xhigh',
},
{
name: 'High',
value: 'high',
},
{
name: 'Low',
value: 'low',
},
{
name: 'Medium',
value: 'medium',
},
{
name: 'Minimal',
value: 'minimal',
},
],
},
{
displayName: 'Frequency Penalty',
name: 'frequencyPenalty',
@ -375,6 +405,7 @@ export class LmChatOpenRouter implements INodeType {
const modelName = this.getNodeParameter('model', itemIndex) as string;
const options = this.getNodeParameter('options', itemIndex, {}) as {
reasoningEffort?: 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
frequencyPenalty?: number;
maxTokens?: number;
maxRetries: number;
@ -411,50 +442,52 @@ export class LmChatOpenRouter implements INodeType {
},
};
const { reasoningEffort, providerRouting, responseFormat, ...modelOptions } = options;
// Build provider routing object
const provider: Record<string, unknown> = {};
if (options.providerRouting) {
const routing = options.providerRouting;
if (routing.order) {
provider.order = routing.order
if (providerRouting) {
if (providerRouting.order) {
provider.order = providerRouting.order
.split(',')
.map((p) => p.trim())
.filter((p) => p);
}
if (routing.allowFallbacks !== undefined) {
provider.allow_fallbacks = routing.allowFallbacks;
if (providerRouting.allowFallbacks !== undefined) {
provider.allow_fallbacks = providerRouting.allowFallbacks;
}
if (routing.requireParameters !== undefined) {
provider.require_parameters = routing.requireParameters;
if (providerRouting.requireParameters !== undefined) {
provider.require_parameters = providerRouting.requireParameters;
}
if (routing.dataCollection) {
provider.data_collection = routing.dataCollection;
if (providerRouting.dataCollection) {
provider.data_collection = providerRouting.dataCollection;
}
if (routing.zdr !== undefined) {
provider.zdr = routing.zdr;
if (providerRouting.zdr !== undefined) {
provider.zdr = providerRouting.zdr;
}
if (routing.only) {
provider.only = routing.only
if (providerRouting.only) {
provider.only = providerRouting.only
.split(',')
.map((p) => p.trim())
.filter((p) => p);
}
if (routing.ignore) {
provider.ignore = routing.ignore
if (providerRouting.ignore) {
provider.ignore = providerRouting.ignore
.split(',')
.map((p) => p.trim())
.filter((p) => p);
}
if (routing.sort) {
provider.sort = routing.sort;
if (providerRouting.sort) {
provider.sort = providerRouting.sort;
}
}
// Build modelKwargs
const modelKwargs: Record<string, unknown> = {};
if (options.responseFormat) {
modelKwargs.response_format = { type: options.responseFormat };
if (reasoningEffort) {
modelKwargs.reasoning = { effort: reasoningEffort };
}
if (responseFormat) {
modelKwargs.response_format = { type: responseFormat };
}
if (Object.keys(provider).length > 0) {
modelKwargs.provider = provider;
@ -463,7 +496,7 @@ export class LmChatOpenRouter implements INodeType {
const model = new ChatOpenAI({
apiKey: credentials.apiKey,
model: modelName,
...options,
...modelOptions,
timeout,
maxRetries: options.maxRetries ?? 2,
configuration,

View File

@ -281,6 +281,80 @@ describe('LmChatOpenRouter', () => {
expect(callArgs?.modelKwargs).toBeUndefined();
});
});
describe('reasoning effort', () => {
const setupWithOptions = (model: string, options: Record<string, unknown>) => {
const ctx = setupMockContext();
ctx.getNodeParameter = vi.fn().mockImplementation((paramName: string) => {
if (paramName === 'model') return model;
if (paramName === 'options') return options;
return undefined;
});
return ctx;
};
it.each(['minimal', 'low', 'medium', 'high', 'xhigh'])(
'should set reasoning.effort to %s in modelKwargs',
async (effort) => {
const ctx = setupWithOptions('openai/gpt-6-sol', { reasoningEffort: effort });
await node.supplyData.call(ctx, 0);
expect(MockedChatOpenAI).toHaveBeenCalledWith(
expect.objectContaining({
modelKwargs: { reasoning: { effort } },
}),
);
},
);
it('should not pass reasoningEffort as a ChatOpenAI field', async () => {
const ctx = setupWithOptions('openai/gpt-6-sol', { reasoningEffort: 'high' });
await node.supplyData.call(ctx, 0);
const callArgs = MockedChatOpenAI.mock.calls[0][0];
expect(callArgs).not.toHaveProperty('reasoningEffort');
expect(callArgs).not.toHaveProperty('reasoning');
});
it('should include reasoning, response format and provider routing together', async () => {
const ctx = setupWithOptions('openai/gpt-6-sol', {
reasoningEffort: 'low',
responseFormat: 'json_object',
providerRouting: { zdr: true },
});
await node.supplyData.call(ctx, 0);
expect(MockedChatOpenAI).toHaveBeenCalledWith(
expect.objectContaining({
modelKwargs: {
reasoning: { effort: 'low' },
response_format: { type: 'json_object' },
provider: { zdr: true },
},
}),
);
});
it.each(['openai/gpt-6-sol', 'openai/gpt-5', 'anthropic/claude-sonnet-4-20250514'])(
'should serialize reasoning in the OpenRouter request for %s',
async (model) => {
const ctx = setupWithOptions(model, { reasoningEffort: 'high' });
await node.supplyData.call(ctx, 0);
// Feed the node's constructor args into the real ChatOpenAI to inspect the request body
const actual =
await vi.importActual<typeof import('@langchain/openai')>('@langchain/openai');
const realModel = new actual.ChatOpenAI(MockedChatOpenAI.mock.calls[0][0]);
const params = realModel.invocationParams({});
expect(params).toMatchObject({ model, reasoning: { effort: 'high' } });
expect(params).not.toHaveProperty('reasoning_effort');
},
);
});
});
describe('fetch wrapper (empty tool call arguments fix)', () => {