@alephantai/n8n-nodes-alephant-analytics-ai 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # @alephantai/n8n-nodes-alephant-analytics-ai
2
+
3
+ n8n community node package for using Alephant analytics as an AI Agent tool.
4
+
5
+ This package adds the **Alephant-Analytics-AI** node to n8n. It reuses the Alephant analytics operations for usage summaries, budget status, costs, recent requests, and request log lookups, but exposes them as an AI tool connection for agent workflows.
6
+
7
+ ## Installation
8
+
9
+ Install this package from n8n Community Nodes:
10
+
11
+ 1. Open n8n.
12
+ 2. Go to **Settings**.
13
+ 3. Open **Community Nodes**.
14
+ 4. Choose **Install**.
15
+ 5. Enter `@alephantai/n8n-nodes-alephant-analytics-ai`.
16
+ 6. Confirm the installation.
17
+
18
+ For self-hosted n8n, restart the instance if your deployment requires it after installing community nodes.
19
+
20
+ ## Authentication
21
+
22
+ The node uses **Alephant Virtual Key** credentials.
23
+
24
+ Create or copy a Virtual Key from Alephant, then add a credential in n8n with:
25
+
26
+ - Virtual Key
27
+ - Optional SaaS Base URL
28
+ - Optional Analytics Base URL
29
+ - Optional Workspace ID
30
+
31
+ The default production URLs are:
32
+
33
+ - SaaS Base URL: `https://alephant.io`
34
+ - Analytics Base URL: `https://analytics.alephant.io`
35
+
36
+ The Alephant quickstart explains the same Virtual Key concept:
37
+
38
+ https://developers.alephant.io/docs/overview/getting-started/quickstart-guide
39
+
40
+ ## AI Agent Usage
41
+
42
+ Add **Alephant-Analytics-AI** to an n8n AI Agent workflow and connect it to the agent tool input.
43
+
44
+ The node exposes `operation` and `period` options with a `filled by AI` choice so the agent can select the analytics operation and time window from the user prompt.
45
+
46
+ Recommended default agent prompts include clear analytics intent, such as:
47
+
48
+ - "Summarize AI usage for the last 7 days."
49
+ - "Check whether the workspace is close to budget."
50
+ - "Show recent AI gateway requests."
51
+ - "Find details for this request log ID."
52
+
53
+ ## Operations
54
+
55
+ The AI tool can run the same analytics operations as the standard Alephant analytics node:
56
+
57
+ - Scope
58
+ - Budget Status
59
+ - Usage Summary
60
+ - Daily Costs
61
+ - Cost by Model
62
+ - Recent Requests
63
+ - Request Log Detail
64
+
65
+ ## Request Log Detail
66
+
67
+ For request log lookups, provide a request log ID in the workflow item or let the AI Agent fill the parameter.
68
+
69
+ The node resolves Workspace ID from:
70
+
71
+ - Node parameter
72
+ - Incoming item JSON
73
+ - Credential Workspace ID
74
+ - Alephant Scope response
75
+
76
+ ## Package Notes
77
+
78
+ This package is intentionally separate from `@alephantai/n8n-nodes-alephant-analytics`.
79
+
80
+ Use the standard analytics package for regular n8n workflow nodes.
81
+
82
+ Use this package when you need the analytics capability as an AI Agent tool.
@@ -0,0 +1,8 @@
1
+ import type { ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class AlephantVirtualKeyApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ test: ICredentialTestRequest;
7
+ properties: INodeProperties[];
8
+ }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AlephantVirtualKeyApi = void 0;
4
+ const constants_1 = require("../shared/constants");
5
+ class AlephantVirtualKeyApi {
6
+ constructor() {
7
+ this.name = 'alephantVirtualKeyApi';
8
+ this.displayName = 'Alephant Virtual Key';
9
+ this.documentationUrl = 'https://developers.alephant.io/n8n';
10
+ this.test = {
11
+ request: {
12
+ baseURL: '={{$credentials.gatewayBaseUrl}}',
13
+ url: '/models',
14
+ headers: {
15
+ Authorization: '=Bearer {{$credentials.virtualKey}}',
16
+ },
17
+ },
18
+ };
19
+ this.properties = [
20
+ {
21
+ displayName: 'Virtual Key',
22
+ name: 'virtualKey',
23
+ type: 'string',
24
+ typeOptions: { password: true },
25
+ default: '',
26
+ required: true,
27
+ description: 'Alephant Virtual Key used for AI Gateway and VK-scoped usage APIs',
28
+ },
29
+ {
30
+ displayName: 'Workspace ID',
31
+ name: 'workspaceId',
32
+ type: 'string',
33
+ default: '',
34
+ required: false,
35
+ description: 'Required for request-log detail lookups with Virtual Key credentials.',
36
+ },
37
+ {
38
+ displayName: 'Gateway Base URL',
39
+ name: 'gatewayBaseUrl',
40
+ type: 'string',
41
+ default: constants_1.DEFAULT_GATEWAY_BASE_URL,
42
+ required: false,
43
+ description: 'Optional. Override for staging, local, or self-hosted Gateway testing.',
44
+ },
45
+ {
46
+ displayName: 'SaaS Base URL',
47
+ name: 'saasBaseUrl',
48
+ type: 'string',
49
+ default: constants_1.DEFAULT_SAAS_BASE_URL,
50
+ required: false,
51
+ description: 'Optional. Override for staging, local, or self-hosted SaaS API testing.',
52
+ },
53
+ {
54
+ displayName: 'Analytics Base URL',
55
+ name: 'analyticsBaseUrl',
56
+ type: 'string',
57
+ default: constants_1.DEFAULT_ANALYTICS_BASE_URL,
58
+ required: false,
59
+ description: 'Optional. Override for staging, local, or self-hosted analytics API testing.',
60
+ },
61
+ ];
62
+ }
63
+ }
64
+ exports.AlephantVirtualKeyApi = AlephantVirtualKeyApi;
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class AlephantAnalyticsAi implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AlephantAnalyticsAi = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const usage_1 = require("../../shared/usage");
6
+ class AlephantAnalyticsAi {
7
+ constructor() {
8
+ this.description = {
9
+ displayName: 'Alephant-Analytics-AI',
10
+ name: 'alephantAnalyticsAi',
11
+ icon: {
12
+ light: 'file:alephant.light.svg',
13
+ dark: 'file:alephant.dark.svg',
14
+ },
15
+ group: ['transform'],
16
+ version: 1,
17
+ subtitle: '={{$parameter["operation"]}}',
18
+ description: 'Alephant AI Analytics provides visibility into AI usage, cost, latency, model/provider performance, agent sessions, and request-level traces across your organization.',
19
+ defaults: { name: 'Alephant-Analytics-AI' },
20
+ usableAsTool: true,
21
+ inputs: [],
22
+ outputs: [n8n_workflow_1.NodeConnectionTypes.AiTool],
23
+ credentials: [{ name: 'alephantVirtualKeyApi', required: true }],
24
+ properties: [
25
+ {
26
+ displayName: 'Operation',
27
+ name: 'operation',
28
+ type: 'options',
29
+ default: 'usageSummary',
30
+ description: 'Choose which Alephant analytics operation to run. In AI tool mode, let the model select this from the user prompt.',
31
+ options: [
32
+ { name: 'filled by AI', value: 'usageSummary' },
33
+ { name: 'Budget Status', value: 'budgetStatus' },
34
+ { name: 'Cost by Model', value: 'costByModel' },
35
+ { name: 'Daily Costs', value: 'dailyCosts' },
36
+ { name: 'Recent Requests', value: 'recentRequests' },
37
+ { name: 'Request Log Detail', value: 'requestLogDetail' },
38
+ { name: 'Scope', value: 'scope' },
39
+ { name: 'Usage Summary', value: 'usageSummary' },
40
+ ],
41
+ },
42
+ {
43
+ displayName: 'Period',
44
+ name: 'period',
45
+ type: 'options',
46
+ default: '7d',
47
+ options: [
48
+ { name: 'filled by AI', value: '7d' },
49
+ { name: '24 Hours', value: '24h' },
50
+ { name: '7 Days', value: '7d' },
51
+ { name: '30 Days', value: '30d' },
52
+ { name: '90 Days', value: '90d' },
53
+ ],
54
+ displayOptions: {
55
+ show: {
56
+ operation: ['budgetStatus', 'usageSummary', 'dailyCosts', 'costByModel'],
57
+ },
58
+ },
59
+ },
60
+ {
61
+ displayName: 'Limit',
62
+ name: 'limit',
63
+ type: 'number',
64
+ default: 50,
65
+ typeOptions: { minValue: 1 },
66
+ displayOptions: { show: { operation: ['recentRequests'] } },
67
+ },
68
+ {
69
+ displayName: 'Offset',
70
+ name: 'offset',
71
+ type: 'number',
72
+ default: 0,
73
+ typeOptions: { minValue: 0 },
74
+ displayOptions: { show: { operation: ['recentRequests'] } },
75
+ },
76
+ {
77
+ displayName: 'Request Log ID',
78
+ name: 'requestLogId',
79
+ type: 'string',
80
+ default: '={{$json.requestLogId || $json.requestId || ""}}',
81
+ required: true,
82
+ displayOptions: { show: { operation: ['requestLogDetail'] } },
83
+ },
84
+ {
85
+ displayName: 'Workspace ID',
86
+ name: 'workspaceId',
87
+ type: 'string',
88
+ default: '={{$json.workspaceId || $json.workspace_id || $json.xWorkspaceId || ""}}',
89
+ description: 'Workspace ID for analytics request log lookups. Overrides the Workspace ID in credentials when set.',
90
+ displayOptions: { show: { operation: ['requestLogDetail'] } },
91
+ },
92
+ {
93
+ displayName: 'Request Log Max Attempts',
94
+ name: 'requestLogMaxAttempts',
95
+ type: 'number',
96
+ default: 6,
97
+ typeOptions: { minValue: 1 },
98
+ description: 'Maximum number of consecutive request log lookup attempts',
99
+ displayOptions: { show: { operation: ['requestLogDetail'] } },
100
+ },
101
+ ],
102
+ };
103
+ }
104
+ async execute() {
105
+ return usage_1.executeUsageNode.call(this);
106
+ }
107
+ }
108
+ exports.AlephantAnalyticsAi = AlephantAnalyticsAi;
@@ -0,0 +1,3 @@
1
+ <svg width="193" height="151" viewBox="0 0 193 151" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M34.0762 116.501H67.8613V150.868H124.364V116.501H158.149V150.868H192.226C192.226 150.868 192.226 126.225 192.226 110.21C192.226 101.942 192.304 79.6343 189.078 59.71C184.127 29.1287 162.518 9.71512 131.762 3.32031C125.879 2.09709 113.938 0.582581 106.831 0.174805L103.103 0L75.1426 55.6875H64.6572L92.6172 0C92.5562 0.00105286 85.7861 0.118271 82.1328 0.408203C52.3673 2.50519 31.1642 10.7184 17.126 25.5137C8.62149 34.5424 3.61155 45.2025 1.16504 59.6484C0.0602955 65.8117 0.000214214 108.598 0 108.753V150.868H34.0762V116.501ZM158.522 78.4717C155.076 78.4714 152.282 75.6772 152.282 72.2305C152.282 68.7838 155.076 65.9895 158.522 65.9893C161.969 65.9893 164.764 68.7837 164.764 72.2305C164.764 75.6773 161.969 78.4717 158.522 78.4717Z" fill="white"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="193" height="151" viewBox="0 0 193 151" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M34.0762 116.501H67.8613V150.868H124.364V116.501H158.149V150.868H192.226C192.226 150.868 192.226 126.225 192.226 110.21C192.226 101.942 192.304 79.6343 189.078 59.71C184.127 29.1287 162.518 9.71512 131.762 3.32031C125.879 2.09709 113.938 0.582581 106.831 0.174805L103.103 0L75.1426 55.6875H64.6572L92.6172 0C92.5562 0.00105286 85.7861 0.118271 82.1328 0.408203C52.3673 2.50519 31.1642 10.7184 17.126 25.5137C8.62149 34.5424 3.61155 45.2025 1.16504 59.6484C0.0602955 65.8117 0.000214214 108.598 0 108.753V150.868H34.0762V116.501ZM158.522 78.4717C155.076 78.4714 152.282 75.6772 152.282 72.2305C152.282 68.7838 155.076 65.9895 158.522 65.9893C161.969 65.9893 164.764 68.7837 164.764 72.2305C164.764 75.6773 161.969 78.4717 158.522 78.4717Z" fill="#202020"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="193" height="151" viewBox="0 0 193 151" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M34.0762 116.501H67.8613V150.868H124.364V116.501H158.149V150.868H192.226C192.226 150.868 192.226 126.225 192.226 110.21C192.226 101.942 192.304 79.6343 189.078 59.71C184.127 29.1287 162.518 9.71512 131.762 3.32031C125.879 2.09709 113.938 0.582581 106.831 0.174805L103.103 0L75.1426 55.6875H64.6572L92.6172 0C92.5562 0.00105286 85.7861 0.118271 82.1328 0.408203C52.3673 2.50519 31.1642 10.7184 17.126 25.5137C8.62149 34.5424 3.61155 45.2025 1.16504 59.6484C0.0602955 65.8117 0.000214214 108.598 0 108.753V150.868H34.0762V116.501ZM158.522 78.4717C155.076 78.4714 152.282 75.6772 152.282 72.2305C152.282 68.7838 155.076 65.9895 158.522 65.9893C161.969 65.9893 164.764 68.7837 164.764 72.2305C164.764 75.6773 161.969 78.4717 158.522 78.4717Z" fill="#202020"/>
3
+ </svg>
@@ -0,0 +1,19 @@
1
+ export declare const DEFAULT_GATEWAY_BASE_URL = "https://ai.alephant.io/v1";
2
+ export declare const DEFAULT_SAAS_BASE_URL = "https://alephant.io";
3
+ export declare const DEFAULT_ANALYTICS_BASE_URL = "https://analytics.alephant.io";
4
+ export declare const ENDPOINTS: {
5
+ chatCompletions: string;
6
+ cockpitScope: string;
7
+ cockpitBudgetStatus: string;
8
+ cockpitUsageSummary: string;
9
+ cockpitDailyCosts: string;
10
+ cockpitCostByModel: string;
11
+ cockpitRecentRequests: string;
12
+ analyticsRequestLogs: string;
13
+ agents: string;
14
+ virtualKeys: string;
15
+ models: string;
16
+ analyticsOverview: string;
17
+ analyticsUsage: string;
18
+ analyticsModels: string;
19
+ };
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ENDPOINTS = exports.DEFAULT_ANALYTICS_BASE_URL = exports.DEFAULT_SAAS_BASE_URL = exports.DEFAULT_GATEWAY_BASE_URL = void 0;
4
+ exports.DEFAULT_GATEWAY_BASE_URL = 'https://ai.alephant.io/v1';
5
+ exports.DEFAULT_SAAS_BASE_URL = 'https://alephant.io';
6
+ exports.DEFAULT_ANALYTICS_BASE_URL = 'https://analytics.alephant.io';
7
+ exports.ENDPOINTS = {
8
+ chatCompletions: '/chat/completions',
9
+ cockpitScope: '/api/v1/cockpit/scope',
10
+ cockpitBudgetStatus: '/api/v1/cockpit/budget-status',
11
+ cockpitUsageSummary: '/api/v1/cockpit/usage-summary',
12
+ cockpitDailyCosts: '/api/v1/cockpit/daily-costs',
13
+ cockpitCostByModel: '/api/v1/cockpit/cost-by-model',
14
+ cockpitRecentRequests: '/api/v1/cockpit/recent-requests',
15
+ analyticsRequestLogs: '/v1/analytics/request-logs',
16
+ agents: '/api/v1/agents',
17
+ virtualKeys: '/api/v1/virtual-keys',
18
+ models: '/api/v1/models',
19
+ analyticsOverview: '/api/v1/analytics/overview',
20
+ analyticsUsage: '/api/v1/analytics/usage',
21
+ analyticsModels: '/api/v1/analytics/models',
22
+ };
@@ -0,0 +1,3 @@
1
+ import type { AlephantManagerCredentials, AlephantVirtualKeyCredentials } from './types';
2
+ export declare function resolveVirtualKeyCredentials(raw: AlephantVirtualKeyCredentials): Required<AlephantVirtualKeyCredentials>;
3
+ export declare function resolveManagerCredentials(raw: AlephantManagerCredentials): Required<AlephantManagerCredentials>;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveVirtualKeyCredentials = resolveVirtualKeyCredentials;
4
+ exports.resolveManagerCredentials = resolveManagerCredentials;
5
+ const constants_1 = require("./constants");
6
+ const url_1 = require("./url");
7
+ function resolveVirtualKeyCredentials(raw) {
8
+ return {
9
+ virtualKey: raw.virtualKey,
10
+ workspaceId: raw.workspaceId?.trim() || '',
11
+ gatewayBaseUrl: (0, url_1.trimTrailingSlash)(raw.gatewayBaseUrl || constants_1.DEFAULT_GATEWAY_BASE_URL),
12
+ saasBaseUrl: (0, url_1.trimTrailingSlash)(raw.saasBaseUrl || constants_1.DEFAULT_SAAS_BASE_URL),
13
+ analyticsBaseUrl: (0, url_1.trimTrailingSlash)(raw.analyticsBaseUrl || constants_1.DEFAULT_ANALYTICS_BASE_URL),
14
+ };
15
+ }
16
+ function resolveManagerCredentials(raw) {
17
+ return {
18
+ pat: raw.pat,
19
+ workspaceId: raw.workspaceId,
20
+ saasBaseUrl: (0, url_1.trimTrailingSlash)(raw.saasBaseUrl || constants_1.DEFAULT_SAAS_BASE_URL),
21
+ analyticsBaseUrl: (0, url_1.trimTrailingSlash)(raw.analyticsBaseUrl || constants_1.DEFAULT_ANALYTICS_BASE_URL),
22
+ };
23
+ }
@@ -0,0 +1,3 @@
1
+ import { NodeApiError } from 'n8n-workflow';
2
+ import type { IExecuteFunctions, IHttpRequestMethods } from 'n8n-workflow';
3
+ export declare function toNodeApiError(ctx: IExecuteFunctions, error: unknown, method: IHttpRequestMethods, url: string, itemIndex?: number): NodeApiError;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toNodeApiError = toNodeApiError;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ function isObjectLike(value) {
6
+ return typeof value === 'object' && value !== null;
7
+ }
8
+ function toNodeApiError(ctx, error, method, url, itemIndex) {
9
+ if (error instanceof n8n_workflow_1.NodeApiError) {
10
+ if (itemIndex !== undefined && error.context.itemIndex === undefined) {
11
+ error.context.itemIndex = itemIndex;
12
+ }
13
+ return error;
14
+ }
15
+ const description = `${method} ${url}`;
16
+ if (isObjectLike(error)) {
17
+ const nodeApiError = new n8n_workflow_1.NodeApiError(ctx.getNode(), error, { description, itemIndex });
18
+ nodeApiError.description = description;
19
+ return nodeApiError;
20
+ }
21
+ const message = typeof error === 'string' ? error : 'Alephant request failed';
22
+ return new n8n_workflow_1.NodeApiError(ctx.getNode(), { message }, { description, itemIndex });
23
+ }
@@ -0,0 +1,13 @@
1
+ import type { IDataObject, IExecuteFunctions, IHttpRequestMethods } from 'n8n-workflow';
2
+ export interface AlephantRequestOptions {
3
+ method: IHttpRequestMethods;
4
+ baseUrl: string;
5
+ path: string;
6
+ token: string;
7
+ workspaceId?: string;
8
+ headers?: IDataObject;
9
+ qs?: IDataObject;
10
+ body?: IDataObject;
11
+ itemIndex?: number;
12
+ }
13
+ export declare function alephantRequest<T>(ctx: IExecuteFunctions, options: AlephantRequestOptions): Promise<T>;
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.alephantRequest = alephantRequest;
4
+ const errors_1 = require("./errors");
5
+ const url_1 = require("./url");
6
+ const TRANSIENT_NETWORK_ERROR_CODES = new Set([
7
+ 'ECONNRESET',
8
+ 'ETIMEDOUT',
9
+ 'ECONNABORTED',
10
+ 'EPIPE',
11
+ ]);
12
+ function isObjectLike(value) {
13
+ return typeof value === 'object' && value !== null;
14
+ }
15
+ function isTransientNetworkError(error) {
16
+ if (!isObjectLike(error) || 'response' in error) {
17
+ return false;
18
+ }
19
+ const code = typeof error.code === 'string' ? error.code : '';
20
+ if (TRANSIENT_NETWORK_ERROR_CODES.has(code)) {
21
+ return true;
22
+ }
23
+ const message = typeof error.message === 'string' ? error.message : '';
24
+ return message.includes('Client network socket disconnected before secure TLS connection');
25
+ }
26
+ async function alephantRequest(ctx, options) {
27
+ const url = `${(0, url_1.trimTrailingSlash)(options.baseUrl)}${options.path}`;
28
+ const request = {
29
+ method: options.method,
30
+ url,
31
+ json: true,
32
+ headers: {
33
+ Authorization: `Bearer ${options.token}`,
34
+ 'Content-Type': 'application/json',
35
+ ...(options.workspaceId ? { 'X-Workspace-Id': options.workspaceId } : {}),
36
+ ...(options.headers || {}),
37
+ },
38
+ qs: options.qs,
39
+ body: options.body,
40
+ };
41
+ let lastError;
42
+ for (let attempt = 0; attempt < 2; attempt++) {
43
+ try {
44
+ return (await ctx.helpers.httpRequest(request));
45
+ }
46
+ catch (error) {
47
+ lastError = error;
48
+ if (attempt === 0 && isTransientNetworkError(error)) {
49
+ continue;
50
+ }
51
+ break;
52
+ }
53
+ }
54
+ throw (0, errors_1.toNodeApiError)(ctx, lastError, options.method, url, options.itemIndex);
55
+ }
@@ -0,0 +1 @@
1
+ export declare function parseJsonObjectInput(value: unknown, fieldName: string): Record<string, unknown>;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseJsonObjectInput = parseJsonObjectInput;
4
+ function parseJsonObjectInput(value, fieldName) {
5
+ if (value === undefined || value === null || value === '') {
6
+ return {};
7
+ }
8
+ let parsed = value;
9
+ if (typeof value === 'string') {
10
+ try {
11
+ parsed = JSON.parse(value);
12
+ }
13
+ catch {
14
+ throw new Error(`${fieldName} must be valid JSON`);
15
+ }
16
+ }
17
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
18
+ throw new Error(`${fieldName} must be a JSON object`);
19
+ }
20
+ return parsed;
21
+ }
@@ -0,0 +1,3 @@
1
+ import type { NormalizedChatCompletion } from './types';
2
+ export { trimTrailingSlash } from './url';
3
+ export declare function normalizeChatCompletion(raw: any, requestId?: string): NormalizedChatCompletion;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.trimTrailingSlash = void 0;
4
+ exports.normalizeChatCompletion = normalizeChatCompletion;
5
+ var url_1 = require("./url");
6
+ Object.defineProperty(exports, "trimTrailingSlash", { enumerable: true, get: function () { return url_1.trimTrailingSlash; } });
7
+ function normalizeChatCompletion(raw, requestId) {
8
+ const firstChoice = Array.isArray(raw?.choices) ? raw.choices[0] : undefined;
9
+ const content = firstChoice?.message?.content ??
10
+ firstChoice?.text ??
11
+ '';
12
+ const text = typeof content === 'string' ? content : String(content);
13
+ return {
14
+ raw,
15
+ text,
16
+ usage: raw?.usage ?? {},
17
+ model: raw?.model,
18
+ requestId,
19
+ requestLogId: requestId,
20
+ finishReason: firstChoice?.finish_reason ?? firstChoice?.finishReason,
21
+ };
22
+ }
@@ -0,0 +1,27 @@
1
+ export type InputMode = 'prompt' | 'messages';
2
+ export interface AlephantVirtualKeyCredentials {
3
+ virtualKey: string;
4
+ workspaceId?: string;
5
+ gatewayBaseUrl?: string;
6
+ saasBaseUrl?: string;
7
+ analyticsBaseUrl?: string;
8
+ }
9
+ export interface AlephantManagerCredentials {
10
+ pat: string;
11
+ workspaceId: string;
12
+ saasBaseUrl?: string;
13
+ analyticsBaseUrl?: string;
14
+ }
15
+ export interface ChatMessage {
16
+ role: 'system' | 'user' | 'assistant' | 'tool';
17
+ content: string;
18
+ }
19
+ export interface NormalizedChatCompletion {
20
+ raw: unknown;
21
+ text: string;
22
+ usage: Record<string, unknown>;
23
+ model: string | undefined;
24
+ requestId: string | undefined;
25
+ requestLogId: string | undefined;
26
+ finishReason: string | undefined;
27
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export declare function trimTrailingSlash(value: string): string;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.trimTrailingSlash = trimTrailingSlash;
4
+ function trimTrailingSlash(value) {
5
+ return value.replace(/\/+$/, '');
6
+ }
@@ -0,0 +1,15 @@
1
+ import type { IDataObject, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
2
+ export type UsageOperation = 'scope' | 'budgetStatus' | 'usageSummary' | 'dailyCosts' | 'costByModel' | 'recentRequests' | 'requestLogDetail';
3
+ export interface UsageRequestParams {
4
+ period?: string | null;
5
+ limit?: number | null;
6
+ offset?: number | null;
7
+ requestLogId?: string | null;
8
+ }
9
+ export interface UsageRequest {
10
+ host?: 'saas' | 'analytics';
11
+ path: string;
12
+ qs?: IDataObject;
13
+ }
14
+ export declare function buildUsageRequest(operation: UsageOperation, params: UsageRequestParams): UsageRequest;
15
+ export declare function executeUsageNode(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildUsageRequest = buildUsageRequest;
4
+ exports.executeUsageNode = executeUsageNode;
5
+ const n8n_workflow_1 = require("n8n-workflow");
6
+ const constants_1 = require("./constants");
7
+ const credentials_1 = require("./credentials");
8
+ const http_1 = require("./http");
9
+ function withQs(path, qs) {
10
+ const sanitized = Object.fromEntries(Object.entries(qs).filter(([, value]) => value !== undefined && value !== null && value !== ''));
11
+ return Object.keys(sanitized).length > 0 ? { path, qs: sanitized } : { path };
12
+ }
13
+ function isPositiveNumber(value) {
14
+ return typeof value === 'number' && Number.isFinite(value) && value > 0;
15
+ }
16
+ function isNonNegativeNumber(value) {
17
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0;
18
+ }
19
+ function requireRequestLogId(value) {
20
+ if (typeof value !== 'string' || value.trim() === '') {
21
+ throw new Error('Request Log ID is required');
22
+ }
23
+ return encodeURIComponent(value.trim());
24
+ }
25
+ function normalizeOptionalString(value) {
26
+ return typeof value === 'string' ? value.trim() : '';
27
+ }
28
+ function readPath(source, path) {
29
+ return path.split('.').reduce((current, key) => {
30
+ if (typeof current !== 'object' || current === null) {
31
+ return undefined;
32
+ }
33
+ return current[key];
34
+ }, source);
35
+ }
36
+ function extractWorkspaceIdFromScope(scope) {
37
+ const paths = [
38
+ 'workspaceId',
39
+ 'workspace_id',
40
+ 'xWorkspaceId',
41
+ 'workspace.id',
42
+ 'workspace.uuid',
43
+ 'data.workspaceId',
44
+ 'data.workspace_id',
45
+ 'data.workspace.id',
46
+ 'data.workspace.uuid',
47
+ 'scope.workspaceId',
48
+ 'scope.workspace_id',
49
+ 'scope.workspace.id',
50
+ 'scope.workspace.uuid',
51
+ ];
52
+ for (const path of paths) {
53
+ const workspaceId = normalizeOptionalString(readPath(scope, path));
54
+ if (workspaceId !== '') {
55
+ return workspaceId;
56
+ }
57
+ }
58
+ return '';
59
+ }
60
+ function normalizeAttemptCount(value) {
61
+ return typeof value === 'number' && Number.isFinite(value) && value > 0
62
+ ? Math.floor(value)
63
+ : 6;
64
+ }
65
+ function isRequestLogNotFoundError(error) {
66
+ const httpCode = normalizeOptionalString(readPath(error, 'httpCode'));
67
+ const status = readPath(error, 'response.status') ?? readPath(error, 'status');
68
+ const code = readPath(error, 'context.data.code') ?? readPath(error, 'response.data.code');
69
+ const message = normalizeOptionalString(readPath(error, 'context.data.message')) ||
70
+ normalizeOptionalString(readPath(error, 'response.data.message')) ||
71
+ normalizeOptionalString(readPath(error, 'message'));
72
+ return ((httpCode === '404' || status === 404) &&
73
+ (code === 40401 || message.toLowerCase().includes('request not found')));
74
+ }
75
+ async function requestWithLogPolling(ctx, options, maxAttempts) {
76
+ let lastError;
77
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
78
+ try {
79
+ return await (0, http_1.alephantRequest)(ctx, options);
80
+ }
81
+ catch (error) {
82
+ lastError = error;
83
+ if (attempt >= maxAttempts || !isRequestLogNotFoundError(error)) {
84
+ throw error;
85
+ }
86
+ }
87
+ }
88
+ throw lastError;
89
+ }
90
+ function buildUsageRequest(operation, params) {
91
+ switch (operation) {
92
+ case 'scope':
93
+ return { path: constants_1.ENDPOINTS.cockpitScope };
94
+ case 'budgetStatus':
95
+ return withQs(constants_1.ENDPOINTS.cockpitBudgetStatus, { period: params.period });
96
+ case 'usageSummary':
97
+ return withQs(constants_1.ENDPOINTS.cockpitUsageSummary, { period: params.period });
98
+ case 'dailyCosts':
99
+ return withQs(constants_1.ENDPOINTS.cockpitDailyCosts, { period: params.period });
100
+ case 'costByModel':
101
+ return withQs(constants_1.ENDPOINTS.cockpitCostByModel, { period: params.period });
102
+ case 'recentRequests':
103
+ return withQs(constants_1.ENDPOINTS.cockpitRecentRequests, {
104
+ limit: isPositiveNumber(params.limit) ? params.limit : undefined,
105
+ offset: isNonNegativeNumber(params.offset) ? params.offset : undefined,
106
+ });
107
+ case 'requestLogDetail':
108
+ return {
109
+ host: 'analytics',
110
+ path: `${constants_1.ENDPOINTS.analyticsRequestLogs}/${requireRequestLogId(params.requestLogId)}`,
111
+ };
112
+ }
113
+ }
114
+ async function executeUsageNode() {
115
+ const items = this.getInputData();
116
+ const credentials = (0, credentials_1.resolveVirtualKeyCredentials)((await this.getCredentials('alephantVirtualKeyApi')));
117
+ const returnData = [];
118
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
119
+ const operation = this.getNodeParameter('operation', itemIndex);
120
+ let request;
121
+ try {
122
+ request = buildUsageRequest(operation, {
123
+ period: this.getNodeParameter('period', itemIndex, '7d'),
124
+ limit: this.getNodeParameter('limit', itemIndex, 50),
125
+ offset: this.getNodeParameter('offset', itemIndex, 0),
126
+ requestLogId: this.getNodeParameter('requestLogId', itemIndex, ''),
127
+ });
128
+ }
129
+ catch (error) {
130
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error instanceof Error ? error.message : 'Invalid Alephant Usage input', { itemIndex });
131
+ }
132
+ let workspaceId = request.host === 'analytics'
133
+ ? normalizeOptionalString(this.getNodeParameter('workspaceId', itemIndex, '')) ||
134
+ normalizeOptionalString(items[itemIndex]?.json?.workspaceId) ||
135
+ normalizeOptionalString(items[itemIndex]?.json?.workspace_id) ||
136
+ normalizeOptionalString(items[itemIndex]?.json?.xWorkspaceId) ||
137
+ credentials.workspaceId
138
+ : undefined;
139
+ if (request.host === 'analytics' && workspaceId === '') {
140
+ const scope = await (0, http_1.alephantRequest)(this, {
141
+ method: 'GET',
142
+ baseUrl: credentials.saasBaseUrl,
143
+ path: constants_1.ENDPOINTS.cockpitScope,
144
+ token: credentials.virtualKey,
145
+ itemIndex,
146
+ });
147
+ workspaceId = extractWorkspaceIdFromScope(scope);
148
+ }
149
+ if (request.host === 'analytics' && workspaceId === '') {
150
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Workspace ID is required for request log detail lookups and could not be resolved from Alephant Scope', { itemIndex });
151
+ }
152
+ const requestOptions = {
153
+ method: 'GET',
154
+ baseUrl: request.host === 'analytics' ? credentials.analyticsBaseUrl : credentials.saasBaseUrl,
155
+ path: request.path,
156
+ token: credentials.virtualKey,
157
+ workspaceId,
158
+ qs: request.qs,
159
+ itemIndex,
160
+ };
161
+ const data = request.host === 'analytics'
162
+ ? await requestWithLogPolling(this, requestOptions, normalizeAttemptCount(this.getNodeParameter('requestLogMaxAttempts', itemIndex, 6)))
163
+ : await (0, http_1.alephantRequest)(this, requestOptions);
164
+ returnData.push({
165
+ json: data,
166
+ pairedItem: { item: itemIndex },
167
+ });
168
+ }
169
+ return [returnData];
170
+ }
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@alephantai/n8n-nodes-alephant-analytics-ai",
3
+ "version": "0.1.0",
4
+ "description": "n8n AI tool node for Alephant analytics",
5
+ "license": "MIT",
6
+ "homepage": "https://alephant.io",
7
+ "author": {
8
+ "name": "Alephant",
9
+ "email": "dev@alephant.io",
10
+ "url": "https://alephant.io"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/AlephantAI/alephant-n8n.git",
15
+ "directory": "packages/alephant-analytics-ai"
16
+ },
17
+ "keywords": [
18
+ "n8n-community-node-package",
19
+ "n8n",
20
+ "alephant",
21
+ "analytics",
22
+ "ai",
23
+ "agent",
24
+ "tool",
25
+ "finops"
26
+ ],
27
+ "main": "dist/index.js",
28
+ "types": "dist/index.d.ts",
29
+ "scripts": {
30
+ "clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
31
+ "build": "npm run clean && tsc -p tsconfig.json && node ../../scripts/copy-package-assets.cjs dist AlephantAnalyticsAi",
32
+ "lint": "npm --prefix ../.. run lint",
33
+ "test": "npm --prefix ../.. run test",
34
+ "prepublishOnly": "npm run lint && npm run test && npm run build"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "README.md",
39
+ "package.json"
40
+ ],
41
+ "n8n": {
42
+ "n8nNodesApiVersion": 1,
43
+ "credentials": [
44
+ "dist/credentials/AlephantVirtualKeyApi.credentials.js"
45
+ ],
46
+ "nodes": [
47
+ "dist/nodes/AlephantAnalyticsAi/AlephantAnalyticsAi.node.js"
48
+ ]
49
+ },
50
+ "peerDependencies": {
51
+ "n8n-workflow": "*"
52
+ },
53
+ "devDependencies": {
54
+ "n8n-workflow": "*",
55
+ "typescript": "^5.6.3"
56
+ }
57
+ }