diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatOpenRouter/LmChatOpenRouter.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatOpenRouter/LmChatOpenRouter.node.ts
index e12430c6e1f..44359363fb3 100644
--- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatOpenRouter/LmChatOpenRouter.node.ts
+++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatOpenRouter/LmChatOpenRouter.node.ts
@@ -275,6 +275,91 @@ export class LmChatOpenRouter implements INodeType {
'Controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered. We generally recommend altering this or temperature but not both.',
type: 'number',
},
+ {
+ displayName: 'Provider Routing',
+ name: 'providerRouting',
+ type: 'collection',
+ default: {},
+ description:
+ 'Configure which sub-providers handle your requests. Learn more.',
+ placeholder: 'Add Provider Routing Option',
+ options: [
+ {
+ displayName: 'Order',
+ name: 'order',
+ type: 'string',
+ default: '',
+ placeholder: 'anthropic,openai,google',
+ description:
+ 'Comma-separated list of provider slugs to try in order. Learn more.',
+ },
+ {
+ displayName: 'Allow Fallbacks',
+ name: 'allowFallbacks',
+ type: 'boolean',
+ default: true,
+ description: 'Whether to allow backup providers when the primary is unavailable',
+ },
+ {
+ displayName: 'Require Parameters',
+ name: 'requireParameters',
+ type: 'boolean',
+ default: false,
+ description:
+ 'Whether to only use providers that support all parameters in your request',
+ },
+ {
+ displayName: 'Data Collection',
+ name: 'dataCollection',
+ type: 'options',
+ options: [
+ { name: 'Allow', value: 'allow' },
+ { name: 'Deny', value: 'deny' },
+ ],
+ default: 'allow',
+ description:
+ "Select 'Deny' to route requests only through providers that don't collect your data",
+ },
+ {
+ displayName: 'Zero Data Retention (ZDR)',
+ name: 'zdr',
+ type: 'boolean',
+ default: false,
+ description:
+ 'Whether to restrict routing to only providers with Zero Data Retention endpoints',
+ },
+ {
+ displayName: 'Only',
+ name: 'only',
+ type: 'string',
+ default: '',
+ placeholder: 'azure,anthropic',
+ description:
+ 'Comma-separated list of provider slugs to allow for this request. Only these providers will be used. Learn more.',
+ },
+ {
+ displayName: 'Ignore',
+ name: 'ignore',
+ type: 'string',
+ default: '',
+ placeholder: 'anthropic,openai',
+ description: 'Comma-separated list of provider slugs to skip for this request',
+ },
+ {
+ displayName: 'Sort',
+ name: 'sort',
+ type: 'options',
+ options: [
+ { name: 'Price', value: 'price' },
+ { name: 'Throughput', value: 'throughput' },
+ { name: 'Latency', value: 'latency' },
+ ],
+ default: '',
+ description:
+ 'Sort providers by a specific attribute. Disables load balancing and tries providers in order. Learn more.',
+ },
+ ],
+ },
],
},
],
@@ -294,6 +379,16 @@ export class LmChatOpenRouter implements INodeType {
temperature?: number;
topP?: number;
responseFormat?: 'text' | 'json_object';
+ providerRouting?: {
+ order?: string;
+ allowFallbacks?: boolean;
+ requireParameters?: boolean;
+ dataCollection?: 'allow' | 'deny';
+ zdr?: boolean;
+ only?: string;
+ ignore?: string;
+ sort?: 'price' | 'throughput' | 'latency';
+ };
};
const timeout = options.timeout;
@@ -308,6 +403,55 @@ export class LmChatOpenRouter implements INodeType {
},
};
+ // Build provider routing object
+ const provider: Record = {};
+ if (options.providerRouting) {
+ const routing = options.providerRouting;
+
+ if (routing.order) {
+ provider.order = routing.order
+ .split(',')
+ .map((p) => p.trim())
+ .filter((p) => p);
+ }
+ if (routing.allowFallbacks !== undefined) {
+ provider.allow_fallbacks = routing.allowFallbacks;
+ }
+ if (routing.requireParameters !== undefined) {
+ provider.require_parameters = routing.requireParameters;
+ }
+ if (routing.dataCollection) {
+ provider.data_collection = routing.dataCollection;
+ }
+ if (routing.zdr !== undefined) {
+ provider.zdr = routing.zdr;
+ }
+ if (routing.only) {
+ provider.only = routing.only
+ .split(',')
+ .map((p) => p.trim())
+ .filter((p) => p);
+ }
+ if (routing.ignore) {
+ provider.ignore = routing.ignore
+ .split(',')
+ .map((p) => p.trim())
+ .filter((p) => p);
+ }
+ if (routing.sort) {
+ provider.sort = routing.sort;
+ }
+ }
+
+ // Build modelKwargs
+ const modelKwargs: Record = {};
+ if (options.responseFormat) {
+ modelKwargs.response_format = { type: options.responseFormat };
+ }
+ if (Object.keys(provider).length > 0) {
+ modelKwargs.provider = provider;
+ }
+
const model = new ChatOpenAI({
apiKey: credentials.apiKey,
model: modelName,
@@ -316,11 +460,7 @@ export class LmChatOpenRouter implements INodeType {
maxRetries: options.maxRetries ?? 2,
configuration,
callbacks: [new N8nLlmTracing(this)],
- modelKwargs: options.responseFormat
- ? {
- response_format: { type: options.responseFormat },
- }
- : undefined,
+ modelKwargs: Object.keys(modelKwargs).length > 0 ? modelKwargs : undefined,
onFailedAttempt: makeN8nLlmFailedAttemptHandler(this, openAiFailedAttemptHandler),
});
diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatOpenRouter/test/LmChatOpenRouter.test.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatOpenRouter/test/LmChatOpenRouter.test.ts
index 2a44428e22a..ec102bda183 100644
--- a/packages/@n8n/nodes-langchain/nodes/llms/LmChatOpenRouter/test/LmChatOpenRouter.test.ts
+++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatOpenRouter/test/LmChatOpenRouter.test.ts
@@ -166,6 +166,115 @@ describe('LmChatOpenRouter', () => {
const callArgs = MockedChatOpenAI.mock.calls[0][0];
expect(callArgs?.configuration?.fetch).toEqual(expect.any(Function));
});
+
+ describe('provider routing options', () => {
+ it('should handle order option with comma-separated providers and spaces', async () => {
+ const ctx = setupMockContext();
+ ctx.getNodeParameter = vi.fn().mockImplementation((paramName: string) => {
+ if (paramName === 'model') return 'anthropic/claude-sonnet-4-20250514';
+ if (paramName === 'options') {
+ return { providerRouting: { order: ' anthropic , openai ,, google ' } };
+ }
+ return undefined;
+ });
+
+ await node.supplyData.call(ctx, 0);
+
+ expect(MockedChatOpenAI).toHaveBeenCalledWith(
+ expect.objectContaining({
+ modelKwargs: expect.objectContaining({
+ provider: expect.objectContaining({
+ order: ['anthropic', 'openai', 'google'],
+ }),
+ }),
+ }),
+ );
+ });
+
+ it('should map provider routing fields to OpenRouter payload', async () => {
+ const ctx = setupMockContext();
+ ctx.getNodeParameter = vi.fn().mockImplementation((paramName: string) => {
+ if (paramName === 'model') return 'anthropic/claude-sonnet-4-20250514';
+ if (paramName === 'options') {
+ return {
+ providerRouting: {
+ allowFallbacks: false,
+ requireParameters: true,
+ dataCollection: 'deny',
+ zdr: true,
+ only: 'azure,anthropic',
+ ignore: 'google',
+ sort: 'price',
+ },
+ };
+ }
+ return undefined;
+ });
+
+ await node.supplyData.call(ctx, 0);
+
+ expect(MockedChatOpenAI).toHaveBeenCalledWith(
+ expect.objectContaining({
+ modelKwargs: expect.objectContaining({
+ provider: expect.objectContaining({
+ allow_fallbacks: false,
+ require_parameters: true,
+ data_collection: 'deny',
+ zdr: true,
+ only: ['azure', 'anthropic'],
+ ignore: ['google'],
+ sort: 'price',
+ }),
+ }),
+ }),
+ );
+ });
+
+ it('should include response format and provider routing together', async () => {
+ const ctx = setupMockContext();
+ ctx.getNodeParameter = vi.fn().mockImplementation((paramName: string) => {
+ if (paramName === 'model') return 'anthropic/claude-sonnet-4-20250514';
+ if (paramName === 'options') {
+ return {
+ responseFormat: 'json_object',
+ providerRouting: {
+ order: 'anthropic,openai',
+ sort: 'price',
+ },
+ };
+ }
+ return undefined;
+ });
+
+ await node.supplyData.call(ctx, 0);
+
+ expect(MockedChatOpenAI).toHaveBeenCalledWith(
+ expect.objectContaining({
+ modelKwargs: expect.objectContaining({
+ response_format: { type: 'json_object' },
+ provider: expect.objectContaining({
+ order: ['anthropic', 'openai'],
+ sort: 'price',
+ }),
+ }),
+ }),
+ );
+ });
+
+ it('should not add provider to modelKwargs when providerRouting is empty', async () => {
+ const ctx = setupMockContext();
+ ctx.getNodeParameter = vi.fn().mockImplementation((paramName: string) => {
+ if (paramName === 'model') return 'anthropic/claude-sonnet-4-20250514';
+ if (paramName === 'options') return { providerRouting: {} };
+ return undefined;
+ });
+
+ await node.supplyData.call(ctx, 0);
+
+ const callArgs = MockedChatOpenAI.mock.calls[0][0];
+ expect(callArgs?.modelKwargs).toBeUndefined();
+ });
+ });
});
describe('fetch wrapper (empty tool call arguments fix)', () => {