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 <cursoragent@cursor.com>
This commit is contained in:
n8n-assistant[bot] 2026-07-02 14:20:20 +00:00 committed by GitHub
parent 4d8304a7c6
commit 2b895252db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 51 additions and 3 deletions

View File

@ -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);
});
});
});

View File

@ -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);
}