mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 21:49:36 +02:00
✨ Allow using separate model for overview task
This commit is contained in:
parent
06ea4e7400
commit
03a148e270
|
|
@ -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 = '';
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
||||
|
|
|
|||
|
|
@ -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<InstanceAiSettingsService>();
|
||||
const aiService = mock<AiService>();
|
||||
const outboundHttp = mock<OutboundHttp>();
|
||||
const globalConfig = mock<GlobalConfig>({ 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(() => {
|
||||
|
|
|
|||
|
|
@ -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<ModelConfig> {
|
||||
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<boolean> {
|
||||
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<ModelConfig> {
|
||||
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<ModelConfig | undefined> {
|
||||
private async resolveHttpProxyModel(
|
||||
user: User,
|
||||
options?: { modelNameOverride?: string },
|
||||
): Promise<ModelConfig | undefined> {
|
||||
// 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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -6502,7 +6502,7 @@ export class InstanceAiService {
|
|||
} = {},
|
||||
): Promise<void> {
|
||||
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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user