n8n/packages/cli/src/services/ai.service.ts
Jaakko Husso cc5f1cb37f
feat(core): Add activation-capped AI Assistant trial variant (no-changelog) (#35994)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 12:46:16 +00:00

108 lines
3.2 KiB
TypeScript

import type {
AiApplySuggestionRequestDto,
AiAskRequestDto,
AiChatRequestDto,
} from '@n8n/api-types';
import { Logger } from '@n8n/backend-common';
import { GlobalConfig } from '@n8n/config';
import { Service } from '@n8n/di';
import { AiAssistantClient } from '@n8n_io/ai-assistant-sdk';
import { ErrorReporter, InstanceSettings } from 'n8n-core';
import { assert, type IUser } from 'n8n-workflow';
import { N8N_VERSION } from '../constants';
import { License } from '../license';
import { callAiServiceWithRetry } from '../utils/ai-service-retry';
@Service()
export class AiService {
private client: AiAssistantClient | undefined;
private initPromise: Promise<void> | undefined;
constructor(
private readonly licenseService: License,
private readonly globalConfig: GlobalConfig,
private readonly instanceSettings: InstanceSettings,
private readonly logger: Logger,
private readonly errorReporter: ErrorReporter,
) {}
async init() {
const aiAssistantEnabled = this.licenseService.isAiAssistantEnabled();
if (!aiAssistantEnabled) {
return;
}
const licenseCert = await this.licenseService.loadCertStr();
const consumerId = this.licenseService.getConsumerId();
const baseUrl = this.globalConfig.aiAssistant.baseUrl;
const logLevel = this.globalConfig.logging.level;
this.client = new AiAssistantClient({
licenseCert,
consumerId,
n8nVersion: N8N_VERSION,
baseUrl,
logLevel,
instanceId: this.instanceSettings.instanceId,
});
// Register for license certificate updates
this.licenseService.onCertRefresh((cert) => {
this.client?.updateLicenseCert(cert);
});
}
async chat(payload: AiChatRequestDto, user: IUser) {
const client = await this.getClient();
return await client.chat(payload, { id: user.id });
}
async applySuggestion(payload: AiApplySuggestionRequestDto, user: IUser) {
const client = await this.getClient();
return await client.applySuggestion(payload, { id: user.id });
}
async askAi(payload: AiAskRequestDto, user: IUser) {
const client = await this.getClient();
return await client.askAi(payload, { id: user.id });
}
/** Whether the AI service proxy is enabled (license + base URL configured). */
isProxyEnabled(): boolean {
return this.licenseService.isAiAssistantEnabled() && !!this.globalConfig.aiAssistant.baseUrl;
}
/** Return the initialized AiAssistantClient. Initializes lazily if needed. */
async getClient(): Promise<AiAssistantClient> {
if (!this.client) {
this.initPromise ??= this.init();
await this.initPromise;
if (!this.client) {
this.initPromise = undefined; // allow retry after license activation
}
}
assert(this.client, 'AI Assistant client not initialized');
return this.client;
}
async createFreeAiCredits(user: IUser) {
const client = await this.getClient();
return await callAiServiceWithRetry(
'AI credits credential generation',
async () => await client.generateAiCreditsCredentials(user),
this.logger,
this.errorReporter,
{ retryOnTimeout: false },
);
}
/** Forfeit the remaining Instance AI quota for this instance. Idempotent server-side. */
async lockInstanceAiQuota(user: IUser, activatedAt?: number) {
const client = await this.getClient();
return await client.lockInstanceAiQuota(user, { activatedAt });
}
}