n8n
Version:
n8n Workflow Automation Tool
219 lines • 10.4 kB
JavaScript
;
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 zod_1 = require("zod");
const builder_tool_names_1 = require("../builder-tool-names");
const llm_provider_defaults_1 = require("../../llm-provider-defaults");
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,
};
}
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 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.'),
}))
.handler(async ({ provider, model, credentialId, }) => {
const all = await deps.credentialProvider.list();
const llmCredentials = all.filter((credential) => llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS[credential.type]);
if (credentialId) {
const credential = llmCredentials.find((c) => c.id === credentialId);
if (!credential) {
return {
ok: false,
reason: 'unknown_credential',
credentialId,
credentials: llmCredentials.map((c) => ({
id: c.id,
name: c.name,
type: c.type,
})),
};
}
const defaults = llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS[credential.type];
if (model?.trim()) {
return await resolveModelAgainstLookup(credential, defaults, model, deps.modelLookup);
}
return toLlmResolution(credential, defaults);
}
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 toLlmResolution(credential, defaults);
}
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 toLlmResolution(credential, defaults);
}
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) {
return {
...toLlmResolution(topCredentials[0], llm_provider_defaults_1.LLM_PROVIDER_DEFAULTS[topCredentials[0].type]),
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