diff --git a/packages/@n8n/config/src/configs/instance-ai.config.ts b/packages/@n8n/config/src/configs/instance-ai.config.ts index 31449704fd9..aac2f7a98fb 100644 --- a/packages/@n8n/config/src/configs/instance-ai.config.ts +++ b/packages/@n8n/config/src/configs/instance-ai.config.ts @@ -16,6 +16,15 @@ export class InstanceAiConfig { @Env('N8N_INSTANCE_AI_MODEL_API_KEY') modelApiKey: string = ''; + /** + * Bare model name (no provider prefix) for lightweight workflow-overview + * generation, e.g. "claude-haiku-4-5". Inherits the main model's provider + * and credentials. Empty = a smaller Anthropic default when the main model + * is Anthropic, otherwise the main model. + */ + @Env('N8N_INSTANCE_AI_OVERVIEW_MODEL') + overviewModel: string = ''; + /** Comma-separated name=url pairs for MCP servers (e.g. "github=https://mcp.github.com/sse"). */ @Env('N8N_INSTANCE_AI_MCP_SERVERS') mcpServers: string = ''; diff --git a/packages/@n8n/instance-ai/docs/configuration.md b/packages/@n8n/instance-ai/docs/configuration.md index e4595a2f954..fa424e194ca 100644 --- a/packages/@n8n/instance-ai/docs/configuration.md +++ b/packages/@n8n/instance-ai/docs/configuration.md @@ -14,6 +14,7 @@ persisted in settings takes precedence over `N8N_INSTANCE_AI_SANDBOX_PROVIDER`. | `N8N_INSTANCE_AI_MODEL` | string | `anthropic/claude-opus-4-8` | LLM model in `provider/model` format for built-in providers, or a bare model name when `N8N_INSTANCE_AI_MODEL_URL` is set. Must be set for the module to enable. | | `N8N_INSTANCE_AI_MODEL_URL` | string | `''` | Base URL for an OpenAI-compatible endpoint (e.g. `http://localhost:1234/v1` for LM Studio). When set, model requests go to this URL instead of the built-in provider. | | `N8N_INSTANCE_AI_MODEL_API_KEY` | string | `''` | API key for the custom model endpoint. Optional — some local servers don't require one. | +| `N8N_INSTANCE_AI_OVERVIEW_MODEL` | string | `''` | Bare model name (no provider prefix) for workflow-overview generation, e.g. `claude-haiku-4-5`. Inherits the main model's provider and credentials. Empty = `claude-sonnet-4-6` when the main model is Anthropic, otherwise the main model. | | `N8N_INSTANCE_AI_MCP_SERVERS` | string | `''` | Comma-separated MCP server configs. Format: `name=url,name=url` | | `N8N_INSTANCE_AI_LOCAL_GATEWAY_DISABLED` | boolean | `false` | Disable the local gateway (filesystem, shell, browser) for all users | diff --git a/packages/cli/src/modules/instance-ai/__tests__/instance-ai-model.service.test.ts b/packages/cli/src/modules/instance-ai/__tests__/instance-ai-model.service.test.ts index 501b901bc26..6c80c042410 100644 --- a/packages/cli/src/modules/instance-ai/__tests__/instance-ai-model.service.test.ts +++ b/packages/cli/src/modules/instance-ai/__tests__/instance-ai-model.service.test.ts @@ -1,5 +1,6 @@ import { UNLIMITED_CREDITS } from '@n8n/api-types'; import type { OutboundHttp } from '@n8n/backend-network'; +import type { GlobalConfig } from '@n8n/config'; import type { User } from '@n8n/db'; import { mock } from 'vitest-mock-extended'; @@ -34,13 +35,14 @@ describe('InstanceAiModelService', () => { const settingsService = mock(); const aiService = mock(); const outboundHttp = mock(); + const globalConfig = mock({ instanceAi: { overviewModel: '' } }); let service: InstanceAiModelService; beforeEach(() => { vi.clearAllMocks(); capturedTokenGetters.length = 0; - service = new InstanceAiModelService(settingsService, aiService, outboundHttp); + service = new InstanceAiModelService(settingsService, aiService, outboundHttp, globalConfig); }); afterEach(() => { diff --git a/packages/cli/src/modules/instance-ai/instance-ai-model.service.ts b/packages/cli/src/modules/instance-ai/instance-ai-model.service.ts index d2e4a967ed7..82982a29c43 100644 --- a/packages/cli/src/modules/instance-ai/instance-ai-model.service.ts +++ b/packages/cli/src/modules/instance-ai/instance-ai-model.service.ts @@ -1,5 +1,6 @@ import { UNLIMITED_CREDITS, buildProxyHeaders, type InstanceAiCredits } from '@n8n/api-types'; import { OutboundHttp } from '@n8n/backend-network'; +import { GlobalConfig } from '@n8n/config'; import type { User } from '@n8n/db'; import { Service } from '@n8n/di'; import type { ModelConfig } from '@n8n/instance-ai'; @@ -13,6 +14,15 @@ import { callAiServiceWithRetry } from '@/utils/ai-service-retry'; import { InstanceAiSettingsService } from './instance-ai-settings.service'; +/** + * Bare model name used for workflow-overview generation when no + * `N8N_INSTANCE_AI_OVERVIEW_MODEL` override is set and the main model is + * Anthropic. Overviews are three schema-bound sentences — a smaller model is + * indistinguishable in quality and much cheaper, and the sidecar runs on + * every user turn. + */ +const DEFAULT_ANTHROPIC_OVERVIEW_MODEL = 'claude-sonnet-4-6'; + /** * Resolves the language model the Instance AI agent runs against and reports * the credit balance for the current user. @@ -35,6 +45,7 @@ export class InstanceAiModelService { private readonly settingsService: InstanceAiSettingsService, private readonly aiService: AiService, private readonly outboundHttp: OutboundHttp, + private readonly globalConfig: GlobalConfig, ) {} /** Whether the AI service proxy is enabled for credit counting. */ @@ -70,6 +81,59 @@ export class InstanceAiModelService { return await this.settingsService.resolveModelConfig(user); } + /** + * Model for lightweight workflow-overview generation (three schema-bound + * sentences): `N8N_INSTANCE_AI_OVERVIEW_MODEL` when set (bare model name, + * inherits the main model's provider + credentials), otherwise a smaller + * Anthropic default when the main model is Anthropic, otherwise the main + * agent model. Follows the same proxy resolution chain as + * {@link resolveAgentModelConfig}. + */ + async resolveOverviewModelConfig(user: User): Promise { + const override = this.globalConfig.instanceAi.overviewModel.trim(); + const hasOverride = override !== ''; + const overrideOrDefault = hasOverride ? override : DEFAULT_ANTHROPIC_OVERVIEW_MODEL; + + if (this.aiService.isProxyEnabled()) { + const client = await this.aiService.getClient(); + const proxyBaseUrl = client.getApiProxyBaseUrl(); + const tokenManager = new ProxyTokenManager(async () => { + return await client.getInstanceAiApiProxyToken( + { id: user.id }, + { userMessageId: nanoid() }, + ); + }); + // The proxy transport is Anthropic-native, so the smaller default is safe here. + return await this.resolveProxyModel(user, proxyBaseUrl, tokenManager, { + modelNameOverride: overrideOrDefault, + }); + } + + // The HTTP-proxy path only ever builds Anthropic models (see the provider gate). + const httpProxyModel = await this.resolveHttpProxyModel(user, { + modelNameOverride: overrideOrDefault, + }); + if (httpProxyModel) return httpProxyModel; + + if (hasOverride) { + return await this.settingsService.resolveModelConfigForVerification(user, override); + } + if (await this.isMainModelAnthropic(user)) { + return await this.settingsService.resolveModelConfigForVerification( + user, + DEFAULT_ANTHROPIC_OVERVIEW_MODEL, + ); + } + return await this.settingsService.resolveModelConfig(user); + } + + /** Whether the resolved main model belongs to the Anthropic provider. */ + private async isMainModelAnthropic(user: User): Promise { + const config = await this.settingsService.resolveModelConfig(user); + const modelId = typeof config === 'string' ? config : 'id' in config ? config.id : null; + return typeof modelId === 'string' && modelId.startsWith('anthropic/'); + } + /** * Build model config. When the AI service proxy is enabled, returns a native * Anthropic LanguageModelV2 instance pointing at the proxy. @@ -87,8 +151,9 @@ export class InstanceAiModelService { user: User, proxyBaseUrl: string, tokenManager: ProxyTokenManager, + options?: { modelNameOverride?: string }, ): Promise { - const modelName = this.settingsService.resolveModelName(user); + const modelName = options?.modelNameOverride ?? this.settingsService.resolveModelName(user); const { createAnthropic } = await import('@ai-sdk/anthropic'); // Route through the proxy-aware transport so this path honours // HTTP(S)_PROXY and the long AI timeout, same as the HTTP-proxy path. @@ -118,7 +183,10 @@ export class InstanceAiModelService { * with a proxy-aware fetch so the AI SDK routes through the proxy. * Returns undefined if no HTTP_PROXY is set or the model isn't anthropic. */ - private async resolveHttpProxyModel(user: User): Promise { + private async resolveHttpProxyModel( + user: User, + options?: { modelNameOverride?: string }, + ): Promise { // Only take over model construction when a proxy is configured; otherwise // the regular model resolution path applies. Node's global `fetch` does // not honour HTTP(S)_PROXY, hence the proxy-aware transport below. @@ -130,7 +198,7 @@ export class InstanceAiModelService { if (!modelId) return undefined; const [provider, ...rest] = modelId.split('/'); - const modelName = rest.join('/'); + const modelName = options?.modelNameOverride ?? rest.join('/'); const apiKey = typeof config === 'object' && 'apiKey' in config ? config.apiKey : undefined; const baseURL = typeof config === 'object' && 'url' in config ? config.url : undefined; if (provider !== 'anthropic') return undefined; diff --git a/packages/cli/src/modules/instance-ai/instance-ai-workflow-overview.service.ts b/packages/cli/src/modules/instance-ai/instance-ai-workflow-overview.service.ts index 360c437579e..d5826f18b9d 100644 --- a/packages/cli/src/modules/instance-ai/instance-ai-workflow-overview.service.ts +++ b/packages/cli/src/modules/instance-ai/instance-ai-workflow-overview.service.ts @@ -53,7 +53,7 @@ export class InstanceAiWorkflowOverviewService { ]); if (!workflow) throw new NotFoundError(`Workflow ${workflowId} not found`); - const modelId = await this.modelService.resolveAgentModelConfig(user); + const modelId = await this.modelService.resolveOverviewModelConfig(user); const structure = await summarizeWorkflowStructure( workflow.name, workflow.nodes, diff --git a/packages/cli/src/modules/instance-ai/instance-ai.service.ts b/packages/cli/src/modules/instance-ai/instance-ai.service.ts index 82152467370..52afe5257c5 100644 --- a/packages/cli/src/modules/instance-ai/instance-ai.service.ts +++ b/packages/cli/src/modules/instance-ai/instance-ai.service.ts @@ -6502,7 +6502,7 @@ export class InstanceAiService { } = {}, ): Promise { try { - const modelId = await this.modelService.resolveAgentModelConfig(user); + const modelId = await this.modelService.resolveOverviewModelConfig(user); // Last N stored messages, oldest first — service-injected blocks stripped // from user turns so they don't leak into the overview prompt.