From 2b895252db9faa59814d6cabf94367baa85ca442 Mon Sep 17 00:00:00 2001 From: "n8n-assistant[bot]" <100856346+n8n-assistant[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:20:20 +0000 Subject: [PATCH] fix(editor): Show AI Gateway hints for supported nodes used as tools (backport to release-candidate/2.29.x) (#33472) Co-authored-by: Alexander Gekov <40495748+alexander-gekov@users.noreply.github.com> Co-authored-by: Cursor --- .../src/app/stores/aiGateway.store.test.ts | 37 +++++++++++++++++++ .../src/app/stores/aiGateway.store.ts | 17 +++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/packages/frontend/editor-ui/src/app/stores/aiGateway.store.test.ts b/packages/frontend/editor-ui/src/app/stores/aiGateway.store.test.ts index 14cf7225578..4f0a8d9dcb1 100644 --- a/packages/frontend/editor-ui/src/app/stores/aiGateway.store.test.ts +++ b/packages/frontend/editor-ui/src/app/stores/aiGateway.store.test.ts @@ -343,6 +343,20 @@ describe('aiGateway.store', () => { ); }); + it('should fall back to the base node name for Tool-suffixed node types', async () => { + mockGetGatewayConfig.mockResolvedValue(MOCK_CONFIG); + const store = useAiGatewayStore(); + await store.fetchConfig(); + + // "openAiTool" is not a config key, but its base "openAi" is. + expect( + store.isActionSupported('@n8n/n8n-nodes-langchain.openAiTool', 'text', 'message'), + ).toBe(true); + expect(store.isActionSupported('@n8n/n8n-nodes-langchain.openAiTool', 'file', 'upload')).toBe( + false, + ); + }); + describe('operation-only nodes (no resource)', () => { it('should return true when operation is in the OPERATION_ONLY list', async () => { mockGetGatewayConfig.mockResolvedValue(MOCK_CONFIG); @@ -456,6 +470,19 @@ describe('aiGateway.store', () => { expect(store.isNodePropertyHidden(null, 'modelSource')).toBe(false); }); + + it('should fall back to the base node name for Tool-suffixed node types', async () => { + mockGetGatewayConfig.mockResolvedValue(MOCK_CONFIG); + const store = useAiGatewayStore(); + await store.fetchConfig(); + + const toolNode = { + type: 'n8n-nodes-browserbase.browserbaseTool', + credentials: { browserbaseApi: { id: null, name: '', __aiGatewayManaged: true } }, + } as unknown as INode; + + expect(store.isNodePropertyHidden(toolNode, 'modelSource')).toBe(true); + }); }); describe('isNodeTypeVersionSupported()', () => { @@ -516,5 +543,15 @@ describe('aiGateway.store', () => { // node support when config is unloaded is handled by isCredentialTypeSupported / isNodeSupported expect(store.isNodeTypeVersionSupported('some-package.SomeNode', 1.1)).toBe(true); }); + + it('should fall back to the base node name for Tool-suffixed node types', async () => { + mockGetGatewayConfig.mockResolvedValue(CONFIG_WITH_VERSION_REQ); + const store = useAiGatewayStore(); + await store.fetchConfig(); + + // "SomeNodeTool" has no entry, but the base "SomeNode" requires >= 1.1. + expect(store.isNodeTypeVersionSupported('some-package.SomeNodeTool', 1.1)).toBe(true); + expect(store.isNodeTypeVersionSupported('some-package.SomeNodeTool', 1.0)).toBe(false); + }); }); }); diff --git a/packages/frontend/editor-ui/src/app/stores/aiGateway.store.ts b/packages/frontend/editor-ui/src/app/stores/aiGateway.store.ts index 82285af5e58..37af9f7a862 100644 --- a/packages/frontend/editor-ui/src/app/stores/aiGateway.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/aiGateway.store.ts @@ -17,6 +17,11 @@ function toError(e: unknown): Error { return e instanceof Error ? e : new Error(String(e)); } +// Tool-variant node types carry a "Tool"/"HitlTool" suffix (e.g. "openAiTool"), +// but the gateway config is keyed by the base node name ("openAi"). +const stripToolSuffix = (nodeName: string) => + nodeName.replace(/HitlTool$/, '').replace(/Tool$/, ''); + export const useAiGatewayStore = defineStore(STORES.AI_GATEWAY, () => { const rootStore = useRootStore(); @@ -98,7 +103,9 @@ export const useAiGatewayStore = defineStore(STORES.AI_GATEWAY, () => { operation: string, ): boolean { if (!config.value) return true; - const nodeActions = config.value.supportedActions?.[nodeName]; + const nodeActions = + config.value.supportedActions?.[nodeName] ?? + config.value.supportedActions?.[stripToolSuffix(nodeName)]; if (!nodeActions) return true; const ops = nodeActions[resource ?? OPERATION_ONLY]; if (!ops) return false; @@ -106,7 +113,9 @@ export const useAiGatewayStore = defineStore(STORES.AI_GATEWAY, () => { } function isNodeTypeVersionSupported(nodeName: string, typeVersion: number): boolean { - const minVersion = config.value?.minNodeTypeVersion?.[nodeName]; + const minVersion = + config.value?.minNodeTypeVersion?.[nodeName] ?? + config.value?.minNodeTypeVersion?.[stripToolSuffix(nodeName)]; if (minVersion === undefined) return true; return typeVersion >= minVersion; } @@ -119,7 +128,9 @@ export const useAiGatewayStore = defineStore(STORES.AI_GATEWAY, () => { ); if (!hasGatewayCredential) return false; - const properties = config.value?.hiddenNodeProperties?.[node.type]; + const properties = + config.value?.hiddenNodeProperties?.[node.type] ?? + config.value?.hiddenNodeProperties?.[stripToolSuffix(node.type)]; if (!properties) return false; return properties.includes(propertyName); }