diff --git a/packages/@n8n/api-types/src/frontend-settings.ts b/packages/@n8n/api-types/src/frontend-settings.ts index 1fe0fcd85798c..3ce856d6aded0 100644 --- a/packages/@n8n/api-types/src/frontend-settings.ts +++ b/packages/@n8n/api-types/src/frontend-settings.ts @@ -163,6 +163,10 @@ export interface FrontendSettings { pruneTime: number; licensePruneTime: number; }; + aiCredits: { + enabled: boolean; + credits: number; + }; pruning?: { isEnabled: boolean; maxAge: number; diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts index 2691191685c7f..6d742ecc8ca93 100644 --- a/packages/cli/src/constants.ts +++ b/packages/cli/src/constants.ts @@ -93,6 +93,7 @@ export const LICENSE_FEATURES = { AI_ASSISTANT: 'feat:aiAssistant', ASK_AI: 'feat:askAi', COMMUNITY_NODES_CUSTOM_REGISTRY: 'feat:communityNodes:customRegistry', + AI_CREDITS: 'feat:aiCredits', } as const; export const LICENSE_QUOTAS = { @@ -101,6 +102,7 @@ export const LICENSE_QUOTAS = { USERS_LIMIT: 'quota:users', WORKFLOW_HISTORY_PRUNE_LIMIT: 'quota:workflowHistoryPrune', TEAM_PROJECT_LIMIT: 'quota:maxTeamProjects', + AI_CREDITS: 'quota:aiCredits', } as const; export const UNLIMITED_LICENSE_QUOTA = -1; diff --git a/packages/cli/src/controllers/e2e.controller.ts b/packages/cli/src/controllers/e2e.controller.ts index 4430dfc9fa7fc..aa0226c754892 100644 --- a/packages/cli/src/controllers/e2e.controller.ts +++ b/packages/cli/src/controllers/e2e.controller.ts @@ -100,6 +100,7 @@ export class E2EController { [LICENSE_FEATURES.AI_ASSISTANT]: false, [LICENSE_FEATURES.COMMUNITY_NODES_CUSTOM_REGISTRY]: false, [LICENSE_FEATURES.ASK_AI]: false, + [LICENSE_FEATURES.AI_CREDITS]: false, }; private numericFeatures: Record = { @@ -108,6 +109,7 @@ export class E2EController { [LICENSE_QUOTAS.USERS_LIMIT]: -1, [LICENSE_QUOTAS.WORKFLOW_HISTORY_PRUNE_LIMIT]: -1, [LICENSE_QUOTAS.TEAM_PROJECT_LIMIT]: 0, + [LICENSE_QUOTAS.AI_CREDITS]: 0, }; constructor( diff --git a/packages/cli/src/license.ts b/packages/cli/src/license.ts index 59804b6a7d0c4..ded98d3f3c01c 100644 --- a/packages/cli/src/license.ts +++ b/packages/cli/src/license.ts @@ -255,6 +255,10 @@ export class License { return this.isFeatureEnabled(LICENSE_FEATURES.ASK_AI); } + isAiCreditsEnabled() { + return this.isFeatureEnabled(LICENSE_FEATURES.AI_CREDITS); + } + isAdvancedExecutionFiltersEnabled() { return this.isFeatureEnabled(LICENSE_FEATURES.ADVANCED_EXECUTION_FILTERS); } @@ -365,6 +369,10 @@ export class License { return this.getFeatureValue(LICENSE_QUOTAS.VARIABLES_LIMIT) ?? UNLIMITED_LICENSE_QUOTA; } + getAiCredits() { + return this.getFeatureValue(LICENSE_QUOTAS.AI_CREDITS) ?? 0; + } + getWorkflowHistoryPruneLimit() { return ( this.getFeatureValue(LICENSE_QUOTAS.WORKFLOW_HISTORY_PRUNE_LIMIT) ?? UNLIMITED_LICENSE_QUOTA diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index ae7a596005599..34b4b3d6b9b69 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -216,6 +216,10 @@ export class FrontendService { askAi: { enabled: false, }, + aiCredits: { + enabled: false, + credits: 0, + }, workflowHistory: { pruneTime: -1, licensePruneTime: -1, @@ -283,6 +287,7 @@ export class FrontendService { const isS3Licensed = this.license.isBinaryDataS3Licensed(); const isAiAssistantEnabled = this.license.isAiAssistantEnabled(); const isAskAiEnabled = this.license.isAskAiEnabled(); + const isAiCreditsEnabled = this.license.isAiCreditsEnabled(); this.settings.license.planName = this.license.getPlanName(); this.settings.license.consumerId = this.license.getConsumerId(); @@ -343,6 +348,11 @@ export class FrontendService { this.settings.askAi.enabled = isAskAiEnabled; } + if (isAiCreditsEnabled) { + this.settings.aiCredits.enabled = isAiCreditsEnabled; + this.settings.aiCredits.credits = this.license.getAiCredits(); + } + this.settings.mfa.enabled = config.get('mfa.enabled'); this.settings.executionMode = config.getEnv('executions.mode'); diff --git a/packages/editor-ui/src/__tests__/defaults.ts b/packages/editor-ui/src/__tests__/defaults.ts index 3771840d6a2dc..46e35a7d172d9 100644 --- a/packages/editor-ui/src/__tests__/defaults.ts +++ b/packages/editor-ui/src/__tests__/defaults.ts @@ -125,6 +125,10 @@ export const defaultSettings: FrontendSettings = { aiAssistant: { enabled: false, }, + aiCredits: { + enabled: false, + credits: 0, + }, betaFeatures: [], easyAIWorkflowOnboarded: false, }; diff --git a/packages/editor-ui/src/api/ai.ts b/packages/editor-ui/src/api/ai.ts index 378a58b4b1fc1..04c08c78f2f5c 100644 --- a/packages/editor-ui/src/api/ai.ts +++ b/packages/editor-ui/src/api/ai.ts @@ -1,4 +1,4 @@ -import type { IRestApiContext } from '@/Interface'; +import type { ICredentialsResponse, IRestApiContext } from '@/Interface'; import type { AskAiRequest, ChatRequest, ReplaceCodeRequest } from '@/types/assistant.types'; import { makeRestApiRequest, streamRequest } from '@/utils/apiUtils'; import type { IDataObject } from 'n8n-workflow'; @@ -42,3 +42,12 @@ export async function generateCodeForPrompt( forNode, } as IDataObject); } + +export async function claimFreeAiCredits( + ctx: IRestApiContext, + { projectId }: { projectId?: string }, +): Promise { + return await makeRestApiRequest(ctx, 'POST', '/ai/free-credits', { + projectId, + } as IDataObject); +} diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 1c79d1da2712b..62d7f27329c50 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -706,12 +706,19 @@ export const EASY_AI_WORKFLOW_EXPERIMENT = { variant: 'variant', }; +export const AI_CREDITS_EXPERIMENT = { + name: '027_free_openai_calls', + control: 'control', + variant: 'variant', +}; + export const EXPERIMENTS_TO_TRACK = [ TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT, CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name, AI_ASSISTANT_EXPERIMENT.name, CREDENTIAL_DOCS_EXPERIMENT.name, EASY_AI_WORKFLOW_EXPERIMENT.name, + AI_CREDITS_EXPERIMENT.name, ]; export const WORKFLOW_EVALUATION_EXPERIMENT = '025_workflow_evaluation'; diff --git a/packages/editor-ui/src/stores/settings.store.ts b/packages/editor-ui/src/stores/settings.store.ts index 2f9e41a0abf56..194e5b3cc4b54 100644 --- a/packages/editor-ui/src/stores/settings.store.ts +++ b/packages/editor-ui/src/stores/settings.store.ts @@ -98,6 +98,10 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => { const isCloudDeployment = computed(() => settings.value.deployment?.type === 'cloud'); + const isAiCreditsEnabled = computed(() => settings.value.aiCredits?.enabled); + + const aiCreditsQuota = computed(() => settings.value.aiCredits?.credits); + const isSmtpSetup = computed(() => userManagement.value.smtpSetup); const isPersonalizationSurveyEnabled = computed( @@ -425,6 +429,8 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => { isCommunityPlan, isAskAiEnabled, isCanvasV2Enabled, + isAiCreditsEnabled, + aiCreditsQuota, reset, testLdapConnection, getLdapConfig, diff --git a/packages/editor-ui/src/stores/users.store.ts b/packages/editor-ui/src/stores/users.store.ts index 8993bd8ad09dc..b349bcb37f975 100644 --- a/packages/editor-ui/src/stores/users.store.ts +++ b/packages/editor-ui/src/stores/users.store.ts @@ -74,6 +74,8 @@ export const useUsersStore = defineStore(STORES.USERS, () => { const globalRoleName = computed(() => currentUser.value?.role ?? 'default'); + const userClaimedAiCredits = computed(() => currentUser.value?.settings?.userClaimedAiCredits); + const isEasyAIWorkflowOnboardingDone = computed(() => Boolean(currentUser.value?.settings?.easyAIWorkflowOnboarded), ); @@ -388,6 +390,8 @@ export const useUsersStore = defineStore(STORES.USERS, () => { mfaEnabled, globalRoleName, personalizedNodeTypes, + userClaimedAiCredits, + isEasyAIWorkflowOnboardingDone, addUsers, loginWithCookie, initialize, @@ -420,7 +424,6 @@ export const useUsersStore = defineStore(STORES.USERS, () => { sendConfirmationEmail, updateGlobalRole, reset, - isEasyAIWorkflowOnboardingDone, setEasyAIWorkflowOnboardingDone, }; });