UNPKG

n8n

Version:

n8n Workflow Automation Tool

365 lines 17.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.buildResolveLlmTool = buildResolveLlmTool; const tool_1 = require("@n8n/agents/tool"); const model_discovery_1 = require("@n8n/ai-utilities/model-discovery"); const api_types_1 = require("@n8n/api-types"); const zod_1 = require("zod"); const builder_tool_names_1 = require("../builder-tool-names"); const llm_provider_defaults_1 = require("../../llm-provider-defaults"); const N8N_CONNECT_CREDENTIAL_NAME = 'n8n credits'; const FREE_CREDITS_MODEL = 'gpt-5-mini'; async function tryClaimFreeCredits(freeCredits) { try { if (!(await freeCredits.isEligible())) return null; const { credentialId, credentialName } = await freeCredits.claim(); return { ok: true, provider: 'openai', model: FREE_CREDITS_MODEL, credentialId, credentialName, claimedFreeOpenAiCredits: true, }; } catch { return null; } } function findProviderDefault(provider) { const requestedProvider = provider.trim(); return Object.entries(llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS).find(([, defaults]) => defaults.provider === requestedProvider); } function toLlmResolution(credential, defaults, model) { return { ok: true, provider: defaults.provider, model: model?.trim() || defaults.defaultModel, credentialId: credential.id, credentialName: credential.name, }; } async function resolveModelAgainstLookup(credential, defaults, requestedModel, modelLookup) { const trimmedModel = requestedModel.trim(); if (!(0, model_discovery_1.isModelDiscoveryProvider)(defaults.provider) || !trimmedModel) { return toLlmResolution(credential, defaults, requestedModel); } let availableModels; try { availableModels = await modelLookup.list(credential.id, credential.type, defaults.provider); } catch (error) { return { ok: false, reason: 'model_lookup_failed', provider: defaults.provider, requestedModel: trimmedModel, error: error instanceof Error ? error.message : String(error), }; } const lowerHint = trimmedModel.toLowerCase(); const exactMatch = availableModels.find((m) => m.value.toLowerCase() === lowerHint); if (exactMatch) { return toLlmResolution(credential, defaults, exactMatch.value); } const candidates = availableModels.filter((m) => m.value.toLowerCase().includes(lowerHint) || m.name.toLowerCase().includes(lowerHint)); if (candidates.length === 1) { return toLlmResolution(credential, defaults, candidates[0].value); } return { ok: false, reason: 'unknown_model', provider: defaults.provider, requestedModel: trimmedModel, availableModels: candidates.length > 0 ? candidates : availableModels, }; } async function resolveDefaultModelForCredential(credential, defaults, modelLookup) { if (credential.id !== api_types_1.AI_GATEWAY_MANAGED_TAG || !(0, model_discovery_1.isModelDiscoveryProvider)(defaults.provider)) { return toLlmResolution(credential, defaults); } let availableModels; try { availableModels = await modelLookup.list(credential.id, credential.type, defaults.provider); } catch (error) { return { ok: false, reason: 'model_lookup_failed', provider: defaults.provider, requestedModel: defaults.defaultModel, error: error instanceof Error ? error.message : String(error), }; } if (availableModels.length === 0) { return { ok: false, reason: 'unknown_model', provider: defaults.provider, requestedModel: defaults.defaultModel, availableModels, }; } const preferred = availableModels.find((m) => m.value === defaults.defaultModel) ?? availableModels[0]; return toLlmResolution(credential, defaults, preferred.value); } async function resolveManagedCredentialForProvider(provider, model, deps) { const providerEntry = findProviderDefault(provider); if (!providerEntry) { return { ok: false, reason: 'unsupported_provider', provider, supportedProviders: Object.values(llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS).map((defaults) => defaults.provider), }; } const [credentialType, defaults] = providerEntry; const served = (await deps.isProviderServedByGateway?.(defaults.provider)) ?? false; if (!served) { return { ok: false, reason: 'n8n_credits_unsupported_provider', provider: defaults.provider, }; } const managed = { id: api_types_1.AI_GATEWAY_MANAGED_TAG, name: N8N_CONNECT_CREDENTIAL_NAME, type: credentialType, }; if (model?.trim()) { return await resolveModelAgainstLookup(managed, defaults, model, deps.modelLookup); } return await resolveDefaultModelForCredential(managed, defaults, deps.modelLookup); } async function servedGatewayProviders(deps) { const served = new Set(); for (const defaults of Object.values(llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS)) { if ((await deps.isProviderServedByGateway?.(defaults.provider)) ?? false) { served.add(defaults.provider); } } return [...served]; } function buildResolveLlmTool(deps) { return new tool_1.Tool(builder_tool_names_1.BUILDER_TOOLS.RESOLVE_LLM) .description('Resolve the agent main LLM without showing a picker. ' + 'For fresh agents, call it once, silently, before the first config write to detect existing ' + 'credentials — with provider/model when the user named them, otherwise without arguments. ' + 'Also call it whenever the user names or changes a provider or model. ' + 'If provider is given, resolves only that provider; if model is omitted, uses the ' + 'provider default model. For "Anthropic via OpenRouter", pass provider="openrouter" ' + 'and omit model unless the user named a concrete OpenRouter model id. Returns ok=false ' + 'when credentials are missing, unsupported, or ambiguous — during an initial build, do not ' + 'ask; keep building with model "" and include the model choice in the trailing ' + 'finish_setup call, then call resolve_llm again with the answer. For a model ' + 'change on an existing agent, ask immediately and keep the current model and credential until the new one resolves. ' + 'When no matching credential exists and the user is eligible for free OpenAI credits, the tool ' + 'claims them automatically and resolves to openai/gpt-5-mini — the result carries ' + 'claimedFreeOpenAiCredits: true; tell the user free OpenAI credits were set up. When the ' + 'provider has no own credential but n8n credits (the managed option) serves it, the tool ' + 'resolves to the managed credential — the result credentialName is "n8n credits"; persist it ' + 'like any credential and tell the user the model runs on n8n credits. When the user ' + 'explicitly asks to use n8n credits, pass useN8nCredits: true (with provider when named): ' + 'the tool resolves n8n credits for that provider without a picker even if the user has their ' + 'own credential for it, and returns ok=false with reason "n8n_credits_unsupported_provider", ' + '"ambiguous_n8n_credits_provider" (with providers), or "n8n_credits_unavailable" (n8n ' + 'credits serves no provider on this instance) when it cannot. When multiple ' + 'providers each have one credential, the tool auto-picks the recommended provider — the result ' + 'carries autoPicked: true and otherProviders; state the pick as changeable, do not ask to confirm it. ' + 'When the user picks between multiple credentials of one provider, pass the picked credentialId ' + 'from the earlier ambiguous result.') .input(zod_1.z.object({ provider: zod_1.z .string() .optional() .describe('Requested provider, e.g. "anthropic", "openai", or "openrouter".'), model: zod_1.z .string() .optional() .describe('Requested model without the selected provider prefix. For OpenRouter use the routed id, e.g. "anthropic/claude-sonnet-4.6".'), credentialId: zod_1.z .string() .optional() .describe('Credential id picked by the user from an earlier ambiguous resolve_llm result.'), useN8nCredits: zod_1.z .boolean() .optional() .describe('Set true when the user explicitly asked to use n8n credits for the main model. ' + 'Resolves n8n credits for the requested provider even if the user already has ' + 'their own credential for it, and never asks. Pass `provider` when the user named one.'), })) .handler(async ({ provider, model, credentialId, useN8nCredits, }) => { if (useN8nCredits) { if (provider) { return await resolveManagedCredentialForProvider(provider, model, deps); } const served = await servedGatewayProviders(deps); if (served.length === 1) { return await resolveManagedCredentialForProvider(served[0], model, deps); } if (served.length === 0) { return { ok: false, reason: 'n8n_credits_unavailable' }; } return { ok: false, reason: 'ambiguous_n8n_credits_provider', providers: served, }; } const all = await deps.credentialProvider.list(); const ownCredentials = all.filter((credential) => llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS[credential.type]); const managedCredentials = []; for (const [credentialType, defaults] of Object.entries(llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS)) { const hasOwnCredential = ownCredentials.some((c) => c.type === credentialType); if (!hasOwnCredential && ((await deps.isProviderServedByGateway?.(defaults.provider)) ?? false)) { managedCredentials.push({ id: api_types_1.AI_GATEWAY_MANAGED_TAG, name: N8N_CONNECT_CREDENTIAL_NAME, type: credentialType, }); } } const llmCredentials = [...ownCredentials, ...managedCredentials]; if (credentialId) { const matchingCredentials = llmCredentials.filter((c) => c.id === credentialId); if (matchingCredentials.length === 0) { return { ok: false, reason: 'unknown_credential', credentialId, credentials: llmCredentials.map((c) => ({ id: c.id, name: c.name, type: c.type, })), }; } let candidates = matchingCredentials; if (provider) { const providerEntry = findProviderDefault(provider); if (!providerEntry) { return { ok: false, reason: 'unsupported_provider', provider, supportedProviders: Object.values(llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS).map((defaults) => defaults.provider), }; } const [credentialType] = providerEntry; candidates = matchingCredentials.filter((c) => c.type === credentialType); } const credential = candidates.length === 1 ? candidates[0] : undefined; if (!credential) { return { ok: false, reason: 'ambiguous_credential', credentials: matchingCredentials.map((c) => { const defaults = llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS[c.type]; return { id: c.id, name: c.name, type: c.type, provider: defaults.provider, }; }), }; } const defaults = llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS[credential.type]; if (model?.trim()) { return await resolveModelAgainstLookup(credential, defaults, model, deps.modelLookup); } return await resolveDefaultModelForCredential(credential, defaults, deps.modelLookup); } if (provider) { const providerEntry = findProviderDefault(provider); if (!providerEntry) { return { ok: false, reason: 'unsupported_provider', provider, supportedProviders: Object.values(llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS).map((defaults) => defaults.provider), }; } const [credentialType, defaults] = providerEntry; const matchingCredentials = llmCredentials.filter((credential) => credential.type === credentialType); if (matchingCredentials.length === 1) { const credential = matchingCredentials[0]; if (model?.trim()) { return await resolveModelAgainstLookup(credential, defaults, model, deps.modelLookup); } return await resolveDefaultModelForCredential(credential, defaults, deps.modelLookup); } if (matchingCredentials.length === 0 && defaults.provider === 'openai' && !model?.trim()) { const claimed = await tryClaimFreeCredits(deps.freeCredits); if (claimed) return claimed; } return { ok: false, reason: matchingCredentials.length === 0 ? 'missing_credential' : 'ambiguous_credential', provider: defaults.provider, credentialType, credentials: matchingCredentials.map((credential) => ({ id: credential.id, name: credential.name, })), }; } if (llmCredentials.length === 1) { const credential = llmCredentials[0]; const defaults = llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS[credential.type]; if (model?.trim()) { return await resolveModelAgainstLookup(credential, defaults, model, deps.modelLookup); } return await resolveDefaultModelForCredential(credential, defaults, deps.modelLookup); } if (llmCredentials.length === 0 && !model?.trim()) { const claimed = await tryClaimFreeCredits(deps.freeCredits); if (claimed) return claimed; } if (llmCredentials.length > 1 && !model?.trim()) { const byProvider = new Map(); for (const credential of llmCredentials) { const providerName = llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS[credential.type].provider; byProvider.set(providerName, [...(byProvider.get(providerName) ?? []), credential]); } const topProvider = llm_provider_defaults_1.LLM_PROVIDER_PRIORITY.find((candidate) => byProvider.has(candidate)); const topCredentials = topProvider ? byProvider.get(topProvider) : undefined; if (topProvider && topCredentials?.length === 1) { const resolved = await resolveDefaultModelForCredential(topCredentials[0], llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS[topCredentials[0].type], deps.modelLookup); if (!resolved.ok) return resolved; return { ...resolved, autoPicked: true, otherProviders: [...byProvider.keys()].filter((other) => other !== topProvider), }; } } return { ok: false, reason: llmCredentials.length === 0 ? 'missing_credential' : 'ambiguous_provider_or_credential', credentials: llmCredentials.map((credential) => { const defaults = llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS[credential.type]; return { id: credential.id, name: credential.name, type: credential.type, provider: defaults.provider, }; }), }; }) .build(); } //# sourceMappingURL=resolve-llm.tool.js.map