This workflow corresponds to n8n.io template #12537 — we link there as the canonical source.
This workflow follows the Emailsend → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "aqlIB54QNx9xtMjo",
"name": "AI-Powered Multi-Model Research Analysis & Report Generation",
"tags": [],
"nodes": [
{
"id": "7f8374e8-47be-476b-af21-84099ffe0241",
"name": "LLM Request Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
928,
1960
],
"parameters": {
"path": "llm-orchestration",
"options": {},
"httpMethod": "POST",
"responseMode": "lastNode"
},
"typeVersion": 2.1
},
{
"id": "baa4054a-1a64-466d-b099-3f5e3a91241b",
"name": "Workflow Configuration",
"type": "n8n-nodes-base.set",
"position": [
1152,
1960
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "id-1",
"name": "azureHealthEndpoint",
"type": "string",
"value": "<__PLACEHOLDER_VALUE__Azure OpenAI health check endpoint URL__>"
},
{
"id": "id-2",
"name": "awsHealthEndpoint",
"type": "string",
"value": "<__PLACEHOLDER_VALUE__AWS Bedrock health check endpoint URL__>"
},
{
"id": "id-3",
"name": "googleHealthEndpoint",
"type": "string",
"value": "<__PLACEHOLDER_VALUE__Google Vertex AI health check endpoint URL__>"
},
{
"id": "id-4",
"name": "localHealthEndpoint",
"type": "string",
"value": "<__PLACEHOLDER_VALUE__Local model health check endpoint URL__>"
},
{
"id": "id-5",
"name": "azureApiEndpoint",
"type": "string",
"value": "<__PLACEHOLDER_VALUE__Azure OpenAI API endpoint URL__>"
},
{
"id": "id-6",
"name": "awsApiEndpoint",
"type": "string",
"value": "<__PLACEHOLDER_VALUE__AWS Bedrock API endpoint URL__>"
},
{
"id": "id-7",
"name": "googleApiEndpoint",
"type": "string",
"value": "<__PLACEHOLDER_VALUE__Google Vertex AI API endpoint URL__>"
},
{
"id": "id-8",
"name": "localApiEndpoint",
"type": "string",
"value": "<__PLACEHOLDER_VALUE__Local model API endpoint URL__>"
},
{
"id": "id-9",
"name": "maxRetries",
"type": "number",
"value": 3
},
{
"id": "id-10",
"name": "retryBaseDelay",
"type": "number",
"value": 1000
},
{
"id": "id-11",
"name": "circuitBreakerThreshold",
"type": "number",
"value": 5
},
{
"id": "id-12",
"name": "anomalyThreshold",
"type": "number",
"value": 0.8
},
{
"id": "id-13",
"name": "costCeilingDefault",
"type": "number",
"value": 1
},
{
"id": "id-14",
"name": "latencySLODefault",
"type": "number",
"value": 5000
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "5616905c-5cbe-449d-a14c-41bb52a96ef3",
"name": "Parse Request & Validate",
"type": "n8n-nodes-base.code",
"position": [
1376,
1960
],
"parameters": {
"jsCode": "// Parse and validate incoming webhook request\n// Extract task metadata, regulatory constraints, cost ceilings, latency SLOs, quality expectations, and tenant ID\n\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n const body = item.json.body || item.json;\n \n // Initialize validation errors array\n const validationErrors = [];\n \n // Parse and validate required fields\n const prompt = body.prompt || body.query || body.text;\n if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) {\n validationErrors.push('Missing or invalid prompt/query/text');\n }\n \n const tenantId = body.tenantId || body.tenant_id || body.tenant;\n if (!tenantId) {\n validationErrors.push('Missing tenant ID');\n }\n \n // Parse task metadata\n const taskMetadata = {\n taskId: body.taskId || body.task_id || `task_${Date.now()}`,\n taskType: body.taskType || body.task_type || 'general',\n priority: body.priority || 'medium',\n timestamp: new Date().toISOString()\n };\n \n // Parse regulatory constraints\n const regulatoryConstraints = {\n dataResidency: body.dataResidency || body.data_residency || 'any',\n complianceRequirements: body.complianceRequirements || body.compliance_requirements || [],\n piiHandling: body.piiHandling || body.pii_handling || 'standard',\n dataClassification: body.dataClassification || body.data_classification || 'internal'\n };\n \n // Parse cost ceilings\n const costCeilings = {\n maxCostPerRequest: parseFloat(body.maxCostPerRequest || body.max_cost_per_request || 0.10),\n budgetLimit: parseFloat(body.budgetLimit || body.budget_limit || 100.0),\n costOptimizationEnabled: body.costOptimizationEnabled !== false\n };\n \n // Parse latency SLOs\n const latencySLOs = {\n maxLatencyMs: parseInt(body.maxLatencyMs || body.max_latency_ms || 5000),\n p95LatencyMs: parseInt(body.p95LatencyMs || body.p95_latency_ms || 3000),\n p99LatencyMs: parseInt(body.p99LatencyMs || body.p99_latency_ms || 4000),\n timeoutMs: parseInt(body.timeoutMs || body.timeout_ms || 30000)\n };\n \n // Parse quality expectations\n const qualityExpectations = {\n minConfidenceScore: parseFloat(body.minConfidenceScore || body.min_confidence_score || 0.7),\n requireFactChecking: body.requireFactChecking || body.require_fact_checking || false,\n hallucinationTolerance: body.hallucinationTolerance || body.hallucination_tolerance || 'low',\n outputFormat: body.outputFormat || body.output_format || 'json',\n maxTokens: parseInt(body.maxTokens || body.max_tokens || 2000),\n temperature: parseFloat(body.temperature || 0.7)\n };\n \n // Validate numeric ranges\n if (costCeilings.maxCostPerRequest < 0) {\n validationErrors.push('maxCostPerRequest must be non-negative');\n }\n \n if (latencySLOs.maxLatencyMs < 100 || latencySLOs.maxLatencyMs > 60000) {\n validationErrors.push('maxLatencyMs must be between 100 and 60000');\n }\n \n if (qualityExpectations.minConfidenceScore < 0 || qualityExpectations.minConfidenceScore > 1) {\n validationErrors.push('minConfidenceScore must be between 0 and 1');\n }\n \n if (qualityExpectations.temperature < 0 || qualityExpectations.temperature > 2) {\n validationErrors.push('temperature must be between 0 and 2');\n }\n \n // Build validated request object\n const validatedRequest = {\n prompt,\n tenantId,\n taskMetadata,\n regulatoryConstraints,\n costCeilings,\n latencySLOs,\n qualityExpectations,\n validationErrors,\n isValid: validationErrors.length === 0,\n rawRequest: body\n };\n \n results.push({\n json: validatedRequest\n });\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "3f668bc0-5e9a-4e27-9d62-b9ce59691728",
"name": "Analyze Prompt Complexity",
"type": "n8n-nodes-base.code",
"position": [
1600,
1960
],
"parameters": {
"jsCode": "// Analyze Prompt Complexity\n// This code analyzes incoming prompts for token count, semantic complexity, domain classification, and processing time estimation\n\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n const prompt = item.json.prompt || item.json.text || item.json.message || '';\n \n // Token count estimation (rough approximation: ~4 chars per token)\n const tokenCount = Math.ceil(prompt.length / 4);\n \n // Semantic complexity score (0-100)\n // Based on: sentence count, avg word length, unique words, punctuation density\n const sentences = prompt.split(/[.!?]+/).filter(s => s.trim().length > 0);\n const words = prompt.split(/\\s+/).filter(w => w.length > 0);\n const uniqueWords = new Set(words.map(w => w.toLowerCase()));\n const avgWordLength = words.reduce((sum, w) => sum + w.length, 0) / (words.length || 1);\n const punctuationCount = (prompt.match(/[,;:()\\[\\]{}\"']/g) || []).length;\n const punctuationDensity = punctuationCount / (words.length || 1);\n \n const complexityScore = Math.min(100, Math.round(\n (sentences.length * 5) +\n (avgWordLength * 8) +\n ((uniqueWords.size / (words.length || 1)) * 30) +\n (punctuationDensity * 20)\n ));\n \n // Domain classification\n const domainKeywords = {\n technical: ['code', 'function', 'algorithm', 'database', 'api', 'programming', 'software', 'debug', 'error', 'system'],\n creative: ['write', 'story', 'poem', 'creative', 'imagine', 'describe', 'narrative', 'character', 'plot'],\n analytical: ['analyze', 'compare', 'evaluate', 'assess', 'calculate', 'determine', 'measure', 'statistics', 'data'],\n conversational: ['hello', 'hi', 'thanks', 'please', 'help', 'question', 'what', 'how', 'why', 'explain'],\n business: ['strategy', 'market', 'revenue', 'customer', 'sales', 'business', 'profit', 'growth', 'roi']\n };\n \n const lowerPrompt = prompt.toLowerCase();\n const domainScores = {};\n \n for (const [domain, keywords] of Object.entries(domainKeywords)) {\n const matchCount = keywords.filter(kw => lowerPrompt.includes(kw)).length;\n domainScores[domain] = matchCount;\n }\n \n const primaryDomain = Object.entries(domainScores)\n .sort((a, b) => b[1] - a[1])[0]?.[0] || 'general';\n \n // Estimated processing time (in seconds)\n // Base time + token factor + complexity factor\n const baseTime = 0.5;\n const tokenFactor = tokenCount * 0.01;\n const complexityFactor = complexityScore * 0.02;\n const estimatedProcessingTime = Math.round((baseTime + tokenFactor + complexityFactor) * 100) / 100;\n \n results.push({\n json: {\n ...item.json,\n promptAnalysis: {\n tokenCount,\n complexityScore,\n primaryDomain,\n domainScores,\n estimatedProcessingTime,\n promptLength: prompt.length,\n sentenceCount: sentences.length,\n wordCount: words.length,\n uniqueWordRatio: Math.round((uniqueWords.size / (words.length || 1)) * 100) / 100\n }\n }\n });\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "7fbcd725-fb37-4be8-b929-280981454365",
"name": "Check Azure OpenAI Health",
"type": "n8n-nodes-base.httpRequest",
"position": [
1824,
1768
],
"parameters": {
"url": "={{ $('Workflow Configuration').first().json.azureHealthEndpoint }}",
"options": {
"timeout": 3000,
"response": {
"response": {
"responseFormat": "json"
}
},
"allowUnauthorizedCerts": true
}
},
"typeVersion": 4.3
},
{
"id": "5cbf7f8d-e33f-4fbf-8976-ce5b3f8f160c",
"name": "Check AWS Bedrock Health",
"type": "n8n-nodes-base.httpRequest",
"position": [
1824,
1960
],
"parameters": {
"url": "={{ $('Workflow Configuration').first().json.awsHealthEndpoint }}",
"options": {
"timeout": 3000,
"response": {
"response": {
"responseFormat": "json"
}
}
}
},
"typeVersion": 4.3
},
{
"id": "a5e5565e-9668-4619-a8f1-7d45b5b72da4",
"name": "Merge Health Checks",
"type": "n8n-nodes-base.merge",
"position": [
2048,
1960
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineAll"
},
"typeVersion": 3.2
},
{
"id": "9155f3f1-8949-4b2d-b2c1-0694e25dec3d",
"name": "Score & Rank Models",
"type": "n8n-nodes-base.code",
"position": [
2272,
1960
],
"parameters": {
"jsCode": "// Score & Rank Models based on multiple criteria\n// Inputs: health checks, historical performance, prompt complexity, config\n\nconst items = $input.all();\n\n// Extract data from merged inputs\nconst healthChecks = [];\nconst historicalPerformance = [];\nlet promptComplexity = {};\nlet config = {};\n\n// Parse input items to separate health checks, historical data, and config\nfor (const item of items) {\n if (item.json.type === 'health_check') {\n healthChecks.push(item.json);\n } else if (item.json.type === 'historical_performance') {\n historicalPerformance.push(item.json);\n } else if (item.json.type === 'prompt_complexity') {\n promptComplexity = item.json;\n } else if (item.json.type === 'config') {\n config = item.json;\n }\n}\n\n// Default configuration values\nconst costWeight = config.costWeight || 0.25;\nconst latencyWeight = config.latencyWeight || 0.30;\nconst qualityWeight = config.qualityWeight || 0.30;\nconst healthWeight = config.healthWeight || 0.15;\n\nconst maxCostPerRequest = config.maxCostPerRequest || 0.10;\nconst maxLatencySLO = config.maxLatencySLO || 5000; // ms\nconst minQualityScore = config.minQualityScore || 0.7;\n\n// Model definitions with base characteristics\nconst modelCharacteristics = {\n 'azure_openai': {\n baseCost: 0.03,\n baseLatency: 2000,\n baseQuality: 0.92,\n complexityMultiplier: 1.2\n },\n 'aws_bedrock': {\n baseCost: 0.025,\n baseLatency: 1800,\n baseQuality: 0.90,\n complexityMultiplier: 1.15\n },\n 'google_vertex': {\n baseCost: 0.028,\n baseLatency: 2200,\n baseQuality: 0.91,\n complexityMultiplier: 1.18\n },\n 'local_model': {\n baseCost: 0.001,\n baseLatency: 3500,\n baseQuality: 0.75,\n complexityMultiplier: 1.5\n }\n};\n\n// Function to calculate model score\nfunction calculateModelScore(modelName, health, historical, complexity) {\n const characteristics = modelCharacteristics[modelName];\n if (!characteristics) return 0;\n\n // Health score (0-1)\n const healthScore = health.status === 'healthy' ? 1.0 : \n health.status === 'degraded' ? 0.5 : 0.0;\n\n // Cost score (inverse - lower cost is better)\n const estimatedCost = characteristics.baseCost * (complexity.score || 1);\n const costScore = Math.max(0, 1 - (estimatedCost / maxCostPerRequest));\n\n // Latency score (inverse - lower latency is better)\n const estimatedLatency = characteristics.baseLatency * \n (complexity.score || 1) * \n characteristics.complexityMultiplier;\n const latencyScore = Math.max(0, 1 - (estimatedLatency / maxLatencySLO));\n\n // Quality score (adjusted by historical performance)\n const historicalQuality = historical?.averageQuality || characteristics.baseQuality;\n const qualityScore = Math.min(1, historicalQuality / minQualityScore);\n\n // Weighted total score\n const totalScore = (\n (healthScore * healthWeight) +\n (costScore * costWeight) +\n (latencyScore * latencyWeight) +\n (qualityScore * qualityWeight)\n );\n\n return {\n modelName,\n totalScore: Math.round(totalScore * 1000) / 1000,\n breakdown: {\n healthScore: Math.round(healthScore * 1000) / 1000,\n costScore: Math.round(costScore * 1000) / 1000,\n latencyScore: Math.round(latencyScore * 1000) / 1000,\n qualityScore: Math.round(qualityScore * 1000) / 1000\n },\n estimates: {\n cost: Math.round(estimatedCost * 10000) / 10000,\n latency: Math.round(estimatedLatency),\n quality: Math.round(historicalQuality * 1000) / 1000\n },\n health: health.status,\n meetsConstraints: (\n estimatedCost <= maxCostPerRequest &&\n estimatedLatency <= maxLatencySLO &&\n historicalQuality >= minQualityScore\n )\n };\n}\n\n// Score all models\nconst modelScores = [];\n\nfor (const health of healthChecks) {\n const modelName = health.model;\n const historical = historicalPerformance.find(h => h.model === modelName);\n \n const score = calculateModelScore(modelName, health, historical, promptComplexity);\n modelScores.push(score);\n}\n\n// Sort by total score (descending)\nmodelScores.sort((a, b) => b.totalScore - a.totalScore);\n\n// Add rank\nmodelScores.forEach((score, index) => {\n score.rank = index + 1;\n});\n\n// Filter to only models that meet constraints\nconst eligibleModels = modelScores.filter(m => m.meetsConstraints && m.health !== 'unhealthy');\nconst ineligibleModels = modelScores.filter(m => !m.meetsConstraints || m.health === 'unhealthy');\n\n// Return results\nreturn [{\n json: {\n type: 'model_ranking',\n timestamp: new Date().toISOString(),\n promptComplexity: promptComplexity.score || 1,\n constraints: {\n maxCostPerRequest,\n maxLatencySLO,\n minQualityScore\n },\n weights: {\n cost: costWeight,\n latency: latencyWeight,\n quality: qualityWeight,\n health: healthWeight\n },\n rankedModels: modelScores,\n eligibleModels,\n ineligibleModels,\n recommendedModel: eligibleModels.length > 0 ? eligibleModels[0].modelName : null,\n totalModelsEvaluated: modelScores.length,\n eligibleCount: eligibleModels.length\n }\n}];"
},
"typeVersion": 2
},
{
"id": "d7a2c5ec-9580-4fc2-8112-8a070c53f8e1",
"name": "Check Policy Constraints",
"type": "n8n-nodes-base.if",
"position": [
2496,
1960
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "or",
"conditions": [
{
"id": "id-1",
"operator": {
"type": "string",
"operation": "exists"
},
"leftValue": "={{ $('Score & Rank Models').item.json.dataResidency }}"
},
{
"id": "id-2",
"operator": {
"type": "string",
"operation": "exists"
},
"leftValue": "={{ $('Score & Rank Models').item.json.complianceRequirements }}"
},
{
"id": "id-3",
"operator": {
"type": "boolean",
"operation": "true"
},
"leftValue": "={{ $('Score & Rank Models').item.json.sensitiveDataFlag }}"
}
]
}
},
"typeVersion": 2.3
},
{
"id": "23dba12b-34ca-4e3b-be73-c23628002ec6",
"name": "Apply Policy Routing",
"type": "n8n-nodes-base.code",
"position": [
2720,
1816
],
"parameters": {
"jsCode": "// Apply Policy Routing - Filter and re-rank models based on regulatory constraints\n\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n const rankedModels = item.json.rankedModels || [];\n const promptAnalysis = item.json.promptAnalysis || {};\n const policyConstraints = item.json.policyConstraints || {};\n \n // Extract sensitivity flags\n const containsPII = promptAnalysis.containsPII || false;\n const containsPHI = promptAnalysis.containsPHI || false;\n const containsFinancial = promptAnalysis.containsFinancial || false;\n const dataClassification = promptAnalysis.dataClassification || 'public';\n \n // Define regulatory requirements for each model\n const modelCompliance = {\n 'azure-openai': {\n hipaaCompliant: true,\n gdprCompliant: true,\n soc2Certified: true,\n dataResidency: ['US', 'EU'],\n piiHandling: true\n },\n 'aws-bedrock': {\n hipaaCompliant: true,\n gdprCompliant: true,\n soc2Certified: true,\n dataResidency: ['US', 'EU', 'APAC'],\n piiHandling: true\n },\n 'google-vertex': {\n hipaaCompliant: true,\n gdprCompliant: true,\n soc2Certified: true,\n dataResidency: ['US', 'EU'],\n piiHandling: true\n },\n 'local-model': {\n hipaaCompliant: false,\n gdprCompliant: false,\n soc2Certified: false,\n dataResidency: ['on-premise'],\n piiHandling: false\n }\n };\n \n // Filter models based on policy constraints\n let filteredModels = rankedModels.filter(model => {\n const compliance = modelCompliance[model.provider] || {};\n \n // Check HIPAA compliance for PHI data\n if (containsPHI && !compliance.hipaaCompliant) {\n console.log(`Filtering out ${model.provider}: Not HIPAA compliant`);\n return false;\n }\n \n // Check GDPR compliance for PII data\n if (containsPII && policyConstraints.requireGDPR && !compliance.gdprCompliant) {\n console.log(`Filtering out ${model.provider}: Not GDPR compliant`);\n return false;\n }\n \n // Check data residency requirements\n if (policyConstraints.dataResidency && !compliance.dataResidency.includes(policyConstraints.dataResidency)) {\n console.log(`Filtering out ${model.provider}: Data residency requirement not met`);\n return false;\n }\n \n // Check PII handling capability\n if ((containsPII || containsPHI) && !compliance.piiHandling) {\n console.log(`Filtering out ${model.provider}: Cannot handle PII/PHI data`);\n return false;\n }\n \n // Check classification level restrictions\n if (dataClassification === 'confidential' && model.provider === 'local-model') {\n console.log(`Filtering out ${model.provider}: Not approved for confidential data`);\n return false;\n }\n \n return true;\n });\n \n // Re-rank filtered models with compliance bonus\n filteredModels = filteredModels.map(model => {\n const compliance = modelCompliance[model.provider] || {};\n let complianceBonus = 0;\n \n // Add bonus points for compliance features\n if (compliance.hipaaCompliant) complianceBonus += 5;\n if (compliance.gdprCompliant) complianceBonus += 5;\n if (compliance.soc2Certified) complianceBonus += 3;\n \n // Adjust score with compliance bonus\n const adjustedScore = (model.score || 0) + complianceBonus;\n \n return {\n ...model,\n complianceScore: complianceBonus,\n originalScore: model.score,\n score: adjustedScore,\n complianceDetails: compliance\n };\n });\n \n // Sort by adjusted score (descending)\n filteredModels.sort((a, b) => b.score - a.score);\n \n // Select the top model after policy filtering\n const selectedModel = filteredModels.length > 0 ? filteredModels[0] : null;\n \n if (!selectedModel) {\n throw new Error('No models available that meet policy constraints');\n }\n \n console.log(`Policy routing selected: ${selectedModel.provider} (score: ${selectedModel.score})`);\n \n results.push({\n json: {\n ...item.json,\n filteredModels,\n selectedModel,\n policyApplied: true,\n modelsFiltered: rankedModels.length - filteredModels.length,\n policyReason: {\n containsPII,\n containsPHI,\n containsFinancial,\n dataClassification,\n constraints: policyConstraints\n }\n }\n });\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "8a0c5e8e-9566-4a62-a7bf-4ea58cb7a2e2",
"name": "Route to Selected Model",
"type": "n8n-nodes-base.switch",
"position": [
2944,
1912
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "Azure",
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.selectedModel }}",
"rightValue": "azure"
}
]
},
"renameOutput": true
},
{
"outputKey": "AWS",
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.selectedModel }}",
"rightValue": "aws"
}
]
},
"renameOutput": true
},
{
"outputKey": "Google",
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.selectedModel }}",
"rightValue": "google"
}
]
},
"renameOutput": true
},
{
"outputKey": "Local",
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.selectedModel }}",
"rightValue": "local"
}
]
},
"renameOutput": true
}
]
},
"options": {
"fallbackOutput": "extra",
"renameFallbackOutput": "Fallback"
}
},
"typeVersion": 3.4
},
{
"id": "cf08c853-68c4-4af4-8a5a-20e198b7bfdf",
"name": "Rewrite Prompt for Azure",
"type": "n8n-nodes-base.code",
"position": [
3168,
1480
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Rewrite and optimize prompt for Azure OpenAI format\nconst item = $input.item.json;\n\n// Extract prompt and configuration from previous nodes\nconst userPrompt = item.prompt || item.userPrompt || item.message || '';\nconst complexity = item.complexity || 'medium';\nconst selectedModel = item.selectedModel || 'gpt-4';\nconst maxTokens = item.maxTokens || 2000;\nconst temperature = item.temperature || 0.7;\n\n// Build Azure OpenAI compatible request format\nconst azureRequest = {\n messages: [\n {\n role: 'system',\n content: 'You are a helpful AI assistant. Provide accurate, concise, and well-structured responses.'\n },\n {\n role: 'user',\n content: userPrompt\n }\n ],\n temperature: temperature,\n max_tokens: maxTokens,\n top_p: 0.95,\n frequency_penalty: 0,\n presence_penalty: 0,\n stop: null\n};\n\n// Adjust parameters based on complexity\nif (complexity === 'high') {\n azureRequest.temperature = Math.min(temperature, 0.5);\n azureRequest.max_tokens = Math.max(maxTokens, 3000);\n} else if (complexity === 'low') {\n azureRequest.temperature = Math.min(temperature + 0.2, 1.0);\n azureRequest.max_tokens = Math.min(maxTokens, 1000);\n}\n\n// Add model-specific optimizations\nif (selectedModel.includes('gpt-4')) {\n azureRequest.messages[0].content += ' Use your advanced reasoning capabilities to provide detailed analysis.';\n} else if (selectedModel.includes('gpt-3.5')) {\n azureRequest.messages[0].content += ' Provide clear and efficient responses.';\n}\n\n// Return the formatted request\nreturn {\n json: {\n ...item,\n azureRequest: azureRequest,\n provider: 'azure',\n formattedPrompt: userPrompt,\n systemMessage: azureRequest.messages[0].content,\n rewrittenAt: new Date().toISOString()\n }\n};"
},
"typeVersion": 2
},
{
"id": "e83198a9-a2c4-43fe-a54a-95df1d07be6e",
"name": "Rewrite Prompt for AWS",
"type": "n8n-nodes-base.code",
"position": [
3168,
1720
],
"parameters": {
"jsCode": "// Rewrite and optimize prompt for AWS Bedrock format\nconst items = $input.all();\nconst outputItems = [];\n\nfor (const item of items) {\n const originalPrompt = item.json.prompt || item.json.userPrompt || '';\n const complexity = item.json.complexity || 'medium';\n const selectedModel = item.json.selectedModel || 'anthropic.claude-v2';\n const maxTokens = item.json.maxTokens || 2048;\n const temperature = item.json.temperature || 0.7;\n \n // Extract model provider from model ID\n const modelProvider = selectedModel.split('.')[0];\n \n // Rewrite prompt based on AWS Bedrock best practices\n let rewrittenPrompt = originalPrompt;\n \n // Add system context for better results\n let systemPrompt = 'You are a helpful AI assistant. Provide accurate, concise, and relevant responses.';\n \n // Adjust based on complexity\n if (complexity === 'high') {\n systemPrompt += ' Take your time to think through complex problems step by step.';\n }\n \n // Format request body based on model provider\n let bedrockRequestBody = {};\n \n if (modelProvider === 'anthropic') {\n // Claude format\n bedrockRequestBody = {\n prompt: `\\n\\nHuman: ${rewrittenPrompt}\\n\\nAssistant:`,\n max_tokens_to_sample: maxTokens,\n temperature: temperature,\n top_p: 0.9,\n top_k: 250,\n stop_sequences: ['\\n\\nHuman:']\n };\n } else if (modelProvider === 'ai21') {\n // AI21 Jurassic format\n bedrockRequestBody = {\n prompt: rewrittenPrompt,\n maxTokens: maxTokens,\n temperature: temperature,\n topP: 0.9,\n stopSequences: [],\n countPenalty: {\n scale: 0\n },\n presencePenalty: {\n scale: 0\n },\n frequencyPenalty: {\n scale: 0\n }\n };\n } else if (modelProvider === 'amazon') {\n // Amazon Titan format\n bedrockRequestBody = {\n inputText: rewrittenPrompt,\n textGenerationConfig: {\n maxTokenCount: maxTokens,\n temperature: temperature,\n topP: 0.9,\n stopSequences: []\n }\n };\n } else if (modelProvider === 'cohere') {\n // Cohere format\n bedrockRequestBody = {\n prompt: rewrittenPrompt,\n max_tokens: maxTokens,\n temperature: temperature,\n p: 0.9,\n k: 0,\n stop_sequences: []\n };\n } else {\n // Default format\n bedrockRequestBody = {\n prompt: rewrittenPrompt,\n max_tokens: maxTokens,\n temperature: temperature\n };\n }\n \n outputItems.push({\n json: {\n ...item.json,\n rewrittenPrompt: rewrittenPrompt,\n systemPrompt: systemPrompt,\n bedrockModelId: selectedModel,\n bedrockRequestBody: bedrockRequestBody,\n bedrockRequestBodyString: JSON.stringify(bedrockRequestBody),\n provider: 'aws_bedrock',\n modelProvider: modelProvider,\n inferenceConfig: {\n maxTokens: maxTokens,\n temperature: temperature,\n topP: 0.9\n }\n }\n });\n}\n\nreturn outputItems;"
},
"typeVersion": 2
},
{
"id": "71106353-66c9-4ac6-a0c2-6fb703afa9bf",
"name": "Rewrite Prompt for Google",
"type": "n8n-nodes-base.code",
"position": [
3168,
1912
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Rewrite and optimize prompt for Google Vertex AI format\nconst item = $input.item.json;\n\n// Extract prompt and configuration from previous nodes\nconst originalPrompt = item.prompt || item.userPrompt || item.query || '';\nconst complexity = item.complexity || 'medium';\nconst selectedModel = item.selectedModel || 'gemini-pro';\nconst maxTokens = item.maxTokens || 2048;\nconst temperature = item.temperature || 0.7;\n\n// Map complexity to Vertex AI parameters\nconst complexityConfig = {\n low: { temperature: 0.3, topP: 0.8, topK: 20 },\n medium: { temperature: 0.7, topP: 0.9, topK: 40 },\n high: { temperature: 0.9, topP: 0.95, topK: 60 }\n};\n\nconst config = complexityConfig[complexity] || complexityConfig.medium;\n\n// Optimize prompt for Vertex AI (Gemini models prefer clear, structured prompts)\nlet optimizedPrompt = originalPrompt;\n\n// Add context markers for better Gemini understanding\nif (!optimizedPrompt.includes('Context:') && !optimizedPrompt.includes('Task:')) {\n optimizedPrompt = `Task: ${optimizedPrompt}\\n\\nPlease provide a clear, accurate, and well-structured response.`;\n}\n\n// Build Vertex AI request payload\nconst vertexPayload = {\n instances: [\n {\n content: optimizedPrompt\n }\n ],\n parameters: {\n temperature: temperature || config.temperature,\n maxOutputTokens: maxTokens,\n topP: config.topP,\n topK: config.topK,\n // Safety settings for Vertex AI\n safetySettings: [\n {\n category: 'HARM_CATEGORY_HATE_SPEECH',\n threshold: 'BLOCK_MEDIUM_AND_ABOVE'\n },\n {\n category: 'HARM_CATEGORY_DANGEROUS_CONTENT',\n threshold: 'BLOCK_MEDIUM_AND_ABOVE'\n },\n {\n category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',\n threshold: 'BLOCK_MEDIUM_AND_ABOVE'\n },\n {\n category: 'HARM_CATEGORY_HARASSMENT',\n threshold: 'BLOCK_MEDIUM_AND_ABOVE'\n }\n ]\n }\n};\n\n// Return optimized payload for Google Vertex AI\nreturn {\n json: {\n ...item,\n originalPrompt: originalPrompt,\n optimizedPrompt: optimizedPrompt,\n vertexPayload: vertexPayload,\n provider: 'google-vertex',\n model: selectedModel,\n rewriteTimestamp: new Date().toISOString(),\n promptOptimizations: [\n 'Added task structure',\n 'Applied Vertex AI safety settings',\n 'Configured generation parameters',\n 'Set complexity-based tuning'\n ]\n }\n};"
},
"typeVersion": 2
},
{
"id": "93ff3a10-55ef-4055-bf2c-90cead9e5e5f",
"name": "Rewrite Prompt for Local",
"type": "n8n-nodes-base.code",
"position": [
3168,
2104
],
"parameters": {
"jsCode": "// Rewrite and optimize prompt for local model format\n// Adapts the prompt to the specific local model API structure\n\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n const originalPrompt = item.json.prompt || '';\n const modelConfig = item.json.selectedModel || {};\n const complexity = item.json.complexity || {};\n const metadata = item.json.metadata || {};\n \n // Extract local model specific configuration\n const localModelName = modelConfig.localModelName || 'llama2';\n const maxTokens = modelConfig.maxTokens || 2048;\n const temperature = modelConfig.temperature || 0.7;\n \n // Optimize prompt for local model\n let optimizedPrompt = originalPrompt;\n \n // Add system context if needed for local models\n const systemContext = metadata.systemContext || '';\n if (systemContext) {\n optimizedPrompt = `System: ${systemContext}\\n\\nUser: ${originalPrompt}`;\n }\n \n // Format for common local model APIs (Ollama, LM Studio, etc.)\n const localModelRequest = {\n model: localModelName,\n prompt: optimizedPrompt,\n options: {\n temperature: temperature,\n num_predict: maxTokens,\n top_p: 0.9,\n top_k: 40,\n repeat_penalty: 1.1\n },\n stream: false\n };\n \n // Add complexity-based optimizations\n if (complexity.level === 'high') {\n localModelRequest.options.temperature = Math.min(temperature + 0.1, 1.0);\n localModelRequest.options.num_predict = Math.min(maxTokens * 1.5, 4096);\n }\n \n results.push({\n json: {\n ...item.json,\n rewrittenPrompt: optimizedPrompt,\n localModelRequest: localModelRequest,\n originalPrompt: originalPrompt,\n modelProvider: 'local',\n rewriteTimestamp: new Date().toISOString()\n }\n });\n}\n\nreturn results;"
},
"typeVersion": 2
},
{
"id": "92217e1d-c378-4b3a-a42e-c8cfaa9e46db",
"name": "Call Azure OpenAI",
"type": "n8n-nodes-base.httpRequest",
"position": [
3392,
1480
],
"parameters": {
"url": "={{ $('Workflow Configuration').first().json.azureApiEndpoint }}",
"body": "={{ JSON.stringify($json.requestBody) }}",
"method": "POST",
"options": {
"timeout": "={{ $json.latencySLO || 30000 }}"
},
"sendBody": true,
"contentType": "raw",
"sendHeaders": true,
"authentication": "genericCredentialType",
"rawContentType": "application/json",
"genericAuthType": "httpHeaderAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.3
},
{
"id": "c6388998-9adc-4628-b474-41a5a04be133",
"name": "Call AWS Bedrock",
"type": "n8n-nodes-base.httpRequest",
"position": [
3392,
1720
],
"parameters": {
"url": "={{ $('Workflow Configuration').first().json.awsApiEndpoint }}",
"body": "={{ JSON.stringify($json.requestBody) }}",
"method": "POST",
"options": {
"timeout": "={{ $json.latencySLO || 30000 }}"
},
"sendBody": true,
"contentType": "raw",
"sendHeaders": true,
"authentication": "genericCredentialType",
"rawContentType": "application/json",
"genericAuthType": "httpHeaderAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.3
},
{
"id": "e92653ff-bfbe-4fd3-907f-1152cefae06d",
"name": "Call Google Vertex AI",
"type": "n8n-nodes-base.httpRequest",
"position": [
3392,
1912
],
"parameters": {
"url": "={{ $('Workflow Configuration').first().json.googleApiEndpoint }}",
"body": "={{ JSON.stringify($json.requestBody) }}",
"method": "POST",
"options": {
"timeout": "={{ $json.latencySLO || 30000 }}"
},
"sendBody": true,
"contentType": "raw",
"sendHeaders": true,
"authentication": "genericCredentialType",
"rawContentType": "application/json",
"genericAuthType": "httpHeaderAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.3
},
{
"id": "0842cfa1-25bc-481e-a10e-ad7f6f31e6cc",
"name": "Call Local Model",
"type": "n8n-nodes-base.httpRequest",
"position": [
3392,
2104
],
"parameters": {
"url": "={{ $('Workflow Configuration').first().json.localApiEndpoint }}",
"body": "={{ JSON.stringify($json.requestBody) }}",
"method": "POST",
"options": {
"timeout": "={{ $json.latencySLO || 30000 }}"
},
"sendBody": true,
"contentType": "raw",
"sendHeaders": true,
"rawContentType": "application/json",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.3
},
{
"id": "aaf3d78d-a12f-4a80-b3b0-4f2e5f43f86e",
"name": "Handle Circuit Breaker",
"type": "n8n-nodes-base.code",
"position": [
3616,
1408
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Circuit Breaker Logic for Multi-Cloud LLM Orchestration\n// Tracks failure counts per model, opens circuit after threshold, determines retry/fallback\n\nconst item = $input.item.json;\n\n// Configuration\nconst FAILURE_THRESHOLD = 3;\nconst CIRCUIT_OPEN_DURATION_MS = 60000; // 1 minute\nconst MAX_RETRIES = 2;\n\n// Initialize circuit breaker state (in production, use external state store)\nconst circuitState = $('Workflow Configuration').item.json.circuitState || {};\n\n// Extract model information and response status\nconst modelName = item.selectedModel || item.model || 'unknown';\nconst isSuccess = item.statusCode >= 200 && item.statusCode < 300;\nconst responseTime = item.responseTime || 0;\nconst errorMessage = item.error || '';\n\n// Initialize model state if not exists\nif (!circuitState[modelName]) {\n circuitState[modelName] = {\n failureCount: 0,\n lastFailureTime: null,\n circuitStatus: 'closed', // closed, open, half-open\n consecutiveSuccesses: 0,\n totalRequests: 0,\n totalFailures: 0\n };\n}\n\nconst modelState = circuitState[modelName];\nmodelState.totalRequests++;\n\n// Check current circuit status\nlet shouldRetry = false;\nlet shouldFallback = false;\nlet retryCount = item.retryCount || 0;\n\n// If circuit is open, check if enough time has passed to try half-open\nif (modelState.circuitStatus === 'open') {\n const timeSinceFailure = Date.now() - modelState.lastFailureTime;\n \n if (timeSinceFailure >= CIRCUIT_OPEN_DURATION_MS) {\n // Move to half-open state for testing\n modelState.circuitStatus = 'half-open';\n console.log(`Circuit for ${modelName} moved to HALF-OPEN state`);\n } else {\n // Circuit still open, trigger immediate fallback\n shouldFallback = true;\n console.log(`Circuit for ${modelName} is OPEN. Triggering fallback.`);\n }\n}\n\n// Process current request result\nif (isSuccess) {\n // Success handling\n modelState.consecutiveSuccesses++;\n \n if (modelState.circuitStatus === 'half-open' && modelState.consecutiveSuccesses >= 2) {\n // Close circuit after successful half-open tests\n modelState.circuitStatus = 'closed';\n modelState.failureCount = 0;\n console.log(`Circuit for ${modelName} CLOSED after successful recovery`);\n } else if (modelState.circuitStatus === 'closed') {\n // Reset failure count on success in closed state\n modelState.failureCount = 0;\n }\n \n shouldRetry = false;\n shouldFallback = false;\n \n} else {\n // Failure handling\n modelState.failureCount++;\n modelState.totalFailures++;\n modelState.lastFailureTime = Date.now();\n modelState.consecutiveSuccesses = 0;\n \n console.log(`Failure detected for ${modelName}. Count: ${modelState.failureCount}/${FAILURE_THRESHOLD}`);\n \n // Check if we should open the circuit\n if (modelState.failureCount >= FAILURE_THRESHOLD) {\n modelState.circuitStatus = 'open';\n console.log(`Circuit for ${modelName} OPENED due to ${FAILURE_THRESHOLD} consecutive failures`);\n shouldFallback = true;\n shouldRetry = false;\n } else if (retryCount < MAX_RETRIES) {\n // Still within retry limits and circuit not open\n shouldRetry = true;\n shouldFallback = false;\n retryCount++;\n } else {\n // Exceeded retry limit\n shouldFallback = true;\n shouldRetry = false;\n console.log(`Max retries (${MAX_RETRIES}) exceeded for ${modelName}. Triggering fallback.`);\n }\n}\n\n// Calculate failure rate\nconst failureRate = modelState.totalRequests > 0 \n ? (modelState.totalFailures / modelState.totalRequests * 100).toFixed(2)\n : 0;\n\n// Prepare output\nreturn {\n json: {\n ...item,\n circuitBreaker: {\n modelName,\n circuitStatus: modelState.circuitStatus,\n failureCount: modelState.failureCount,\n failureThreshold: FAILURE_THRESHOLD,\n consecutiveSuccesses: modelState.consecutiveSuccesses,\n totalRequests: modelState.totalRequests,\n totalFailures: modelState.totalFailures,\n failureRate: `${failureRate}%`,\n lastFailureTime: modelState.lastFailureTime,\n shouldRetry,\n shouldFallback,\n retryCount,\n maxRetries: MAX_RETRIES,\n timestamp: Date.now()\n },\n shouldRetry,\n shouldFallback,\n retryCount,\n circuitState // Pass updated state forward\n }\n};"
},
"typeVersion": 2
},
{
"id": "003a5db0-1db5-4593-aa66-5bb6bc1edda5",
"name": "Check Retry Needed",
"type": "n8n-nodes-base.if",
"position": [
3840,
1408
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": false,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "id-1",
"operator": {
"type": "boolean",
"operation": "true"
},
"leftValue": "={{ $('Handle Circuit Breaker').item.json.shouldRetry }}"
},
{
"id": "id-2",
"operator": {
"type": "number",
"operation": "lt"
},
"leftValue": "={{ $('Handle Circuit Breaker').item.json.retryCount }}",
"rightValue": "={{ $('Handle Circuit Breaker').item.json.maxRetries }}"
},
{
"id": "id-3",
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $('Handle Circuit Breaker').item.json.circuitBreakerState }}",
"rightValue": "open"
}
]
}
},
"typeVersion": 2.3
},
{
"id": "6d1bd635-cd97-4e8a-aeb2-bf7bf33e4de6",
"name": "Adaptive Retry Delay",
"type": "n8n-nodes-base.wait",
"position": [
4064,
1552
],
"parameters": {
"amount": "={{ $('Workflow Configuration').first().json.retryBaseDelay * Math.pow(2, $json.retryCount || 0) }}"
},
"typeVersion": 1.1
},
{
"id": "f24bc275-47d1-498b-82b1-4bfabb76907e",
"name": "Merge Model Responses",
"type": "n8n-nodes-base.merge",
"position": [
4064,
1768
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineAll"
},
"typeVersion": 3.2
},
{
"id": "4cf06d1b-698e-4f9f-9dbd-c52083dd5da9",
"name": "Normalize Output",
"type": "n8n-nodes-base.code",
"position": [
4288,
1768
],
"parameters": {
"jsCode": "// Normalize model outputs into a consistent format\n// Extract text, metadata, and provider-specific fields into a unified structure\n\nconst items = $input.all();\nconst normalizedOutputs = [];\n\nfor (const item of items) {\n const rawResponse = item.json;\n \n // Initialize normalized structure\n const normalized = {\n text: '',\n provider: '',\n model: '',\n metadata: {\n tokensUsed: 0,\n promptTokens: 0,\n completionTokens: 0,\n latencyMs: 0,\n finishReason: '',\n requestId: ''\n },\n providerSpecific: {},\n timestamp: new Date().toISOString(),\n success: true\n };\n \n // Detect provider and normalize accordingly\n if (rawResponse.provider === 'azure' || rawResponse.choices) {\n // Azure OpenAI format\n normalized.provider = 'azure';\n normalized.text = rawResponse.choices?.[0]?.message?.content || rawResponse.choices?.[0]?.text || '';\n normalized.model = rawResponse.model || 'gpt-4';\n normalized.metadata.tokensUsed = rawResponse.usage?.total_tokens || 0;\n normalized.metadata.promptTokens = rawResponse.usage?.prompt_tokens || 0;\n normalized.metadata.completionTokens = rawResponse.usage?.completion_tokens || 0;\n normalized.metadata.finishReason = rawResponse.choices?.[0]?.finish_reason || '';\n normalized.metadata.requestId = rawResponse.id || '';\n normalized.providerSpecific = {\n systemFingerprint: rawResponse.system_fingerprint,\n created: rawResponse.created\n };\n } \n else if (rawResponse.provider === 'aws' || rawResponse.body) {\n // AWS Bedrock format\n normalized.provider = 'aws';\n const body = typeof rawResponse.body === 'string' ? JSON.parse(rawResponse.body) : rawResponse.body;\n normalized.text = body.completion || body.results?.[0]?.outputText || '';\n normalized.model = rawResponse.modelId || 'claude-v2';\n normalized.metadata.tokensUsed = body.usage?.total_tokens || (body.inputTextTokenCount || 0) + (body.results?.[0]?.tokenCount || 0);\n normalized.metadata.promptTokens = body.inputTextTokenCount || 0;\n normalized.metadata.completionTokens = body.results?.[0]?.tokenCount || 0;\n normalized.metadata.finishReason = body.stop_reason || body.results?.[0]?.completionReason || '';\n normalized.metadata.requestId = rawResponse.ResponseMetadata?.RequestId || '';\n normalized.providerSpecific = {\n stopReason: body.stop_reason,\n amazonBedrockInvocationMetrics: rawResponse.ResponseMetadata?.HTTPHeaders?.['x-amzn-bedrock-invocation-latency']\n };\n }\n else if (rawResponse.provider === 'google' || rawResponse.predictions) {\n // Google Vertex AI format\n normalized.provider = 'google';\n normalized.text = rawResponse.predictions?.[0]?.content || rawResponse.predictions?.[0]?.candidates?.[0]?.content || '';\n normalized.model = rawResponse.deployedModelId || 'text-bison';\n normalized.metadata.tokensUsed = rawResponse.metadata?.tokenMetadata?.totalTokens || 0;\n normalized.metadata.promptTokens = rawResponse.metadata?.tokenMetadata?.inputTokens || 0;\n normalized.metadata.completionTokens = rawResponse.metadata?.tokenMetadata?.outputTokens || 0;\n normalized.metadata.finishReason = rawResponse.predictions?.[0]?.safetyAttributes?.blocked ? 'blocked' : 'stop';\n normalized.metadata.requestId = rawResponse.metadata?.requestId || '';\n normalized.providerSpecific = {\n safetyAttributes: rawResponse.predictions?.[0]?.safetyAttributes,\n citationMetadata: rawResponse.predictions?.[0]?.citationMetadata\n };\n }\n else if (rawResponse.provider === 'local' || rawResponse.response) {\n // Local model format (e.g., Ollama, LM Studio)\n normalized.provider = 'local';\n normalized.text = rawResponse.response || rawResponse.text || rawResponse.output || '';\n normalized.model = rawResponse.model || 'local-llm';\n normalized.metadata.tokensUsed = rawResponse.eval_count || 0;\n normalized.metadata.promptTokens = rawResponse.prompt_eval_count || 0;\n normalized.metadata.completionTokens = rawResponse.eval_count || 0;\n normalized.metadata.finishReason = rawResponse.done ? 'stop' : 'length';\n normalized.metadata.requestId = rawResponse.created_at || '';\n normalized.providerSpecific = {\n totalDuration: rawResponse.total_duration,\n loadDuration: rawResponse.load_duration,\n evalDuration: rawResponse.eval_duration\n };\n }\n else {\n // Generic fallback\n normalized.provider = rawResponse.provider || 'unknown';\n normalized.text = rawResponse.text || rawResponse.content || rawResponse.output || JSON.stringify(rawResponse);\n normalized.model = rawResponse.model || 'unknown';\n normalized.success = false;\n }\n \n // Calculate latency if available\n if (rawResponse.startTime && rawResponse.endTime) {\n normalized.metadata.latencyMs = rawResponse.endTime - rawResponse.startTime;\n } else if (rawResponse.latency) {\n normalized.metadata.latencyMs = rawResponse.latency;\n }\n \n normalizedOutputs.push({ json: normalized });\n}\n\nreturn normalizedOutputs;"
},
"typeVersion": 2
},
{
"id": "5f4baa3c-b785-4d5a-8314-9e1b87e0314c",
"name": "Calculate Telemetry Metrics",
"type": "n8n-nodes-base.code",
"position": [
4512,
1768
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Calculate Telemetry Metrics\nconst item = $input.item.json;\n\n// Extract model response data\nconst modelResponse = item.modelResponse || {};\nconst selectedModel = item.selectedModel || 'unknown';\nconst startTime = item.startTime || Date.now();\n\n// Calculate token usage\nconst promptTokens = modelResponse.usage?.prompt_tokens || 0;\nconst completionTokens = modelResponse.usage?.completion_tokens || 0;\nconst totalTokens = promptTokens + completionTokens;\n\n// Calculate actual latency (in milliseconds)\nconst endTime = Date.now();\nconst actualLatency = endTime - startTime;\n\n// Model pricing per 1K tokens (adjust based on actual pricing)\nconst modelPricing = {\n 'azure-openai': { prompt: 0.0015, completion: 0.002 },\n 'aws-bedrock': { prompt: 0.0008, completion: 0.0024 },\n 'google-vertex': { prompt: 0.00025, completion: 0.0005 },\n 'local-model': { prompt: 0, completion: 0 }\n};\n\n// Get pricing for selected model\nconst pricing = modelPricing[selectedModel] || { prompt: 0, completion: 0 };\n\n// Calculate cost\nconst promptCost = (promptTokens / 1000) * pricing.prompt;\nconst completionCost = (completionTokens / 1000) * pricing.completion;\nconst totalCost = promptCost + completionCost;\n\n// Create telemetry metrics object\nconst telemetryMetrics = {\n totalTokens: totalTokens,\n promptTokens: promptTokens,\n completionTokens: completionTokens,\n actualLatency: actualLatency,\n latencySeconds: (actualLatency / 1000).toFixed(3),\n cost: {\n promptCost: parseFloat(promptCost.toFixed(6)),\n completionCost: parseFloat(completionCost.toFixed(6)),\n totalCost: parseFloat(totalCost.toFixed(6)),\n currency: 'USD'\n },\n timestamp: new Date().toISOString(),\n model: selectedModel,\n startTime: startTime,\n endTime: endTime\n};\n\n// Return enriched item with telemetry metrics\nreturn {\n json: {\n ...item,\n telemetryMetrics: telemetryMetrics\n }\n};"
},
"typeVersion": 2
},
{
"id": "724e0d2f-097b-4e90-9aa0-f07cc7f1a047",
"name": "Assess Hallucination Risk",
"type": "n8n-nodes-base.code",
"position": [
4736,
1768
],
"parameters": {
"jsCode": "// Assess Hallucination Risk\n// Analyzes LLM response for potential hallucinations using multiple heuristics\n// Produces a risk score from 0 (low risk) to 1 (high risk)\n\nconst items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n const response = item.json.response || item.json.output || '';\n const prompt = item.json.prompt || item.json.input || '';\n const modelName = item.json.selectedModel || 'unknown';\n \n // Initialize risk factors\n let riskScore = 0;\n const riskFactors = [];\n \n // 1. Response Consistency Check\n // Look for contradictions within the response\n const sentences = response.split(/[.!?]+/).filter(s => s.trim().length > 0);\n let contradictionScore = 0;\n \n // Check for common contradiction patterns\n const contradictionPatterns = [\n { pattern: /(however|but|although|despite).*?(not|never|no|none)/gi, weight: 0.1 },\n { pattern: /(always|never|all|none).*?(sometimes|maybe|possibly)/gi, weight: 0.15 },\n { pattern: /(yes|correct|true).*?(no|incorrect|false)/gi, weight: 0.2 }\n ];\n \n for (const { pattern, weight } of contradictionPatterns) {\n const matches = response.match(pattern);\n if (matches && matches.length > 0) {\n contradictionScore += weight * matches.length;\n riskFactors.push(`Contradiction pattern detected (${matches.length} instances)`);\n }\n }\n \n riskScore += Math.min(contradictionScore, 0.3);\n \n // 2. Confidence Markers Analysis\n // Lack of confidence markers or excessive hedging indicates uncertainty\n const uncertaintyMarkers = [\n 'might', 'maybe', 'possibly', 'perhaps', 'could be', 'may be',\n 'i think', 'i believe', 'probably', 'likely', 'unclear',\n 'not sure', 'uncertain', 'approximately', 'roughly'\n ];\n \n let uncertaintyCount = 0;\n const lowerResponse = response.toLowerCase();\n \n for (const marker of uncertaintyMarkers) {\n const regex = new RegExp(marker, 'gi');\n const matches = lowerResponse.match(regex);\n if (matches) {\n uncertaintyCount += matches.length;\n }\n }\n \n // High uncertainty marker density suggests hallucination risk\n const uncertaintyDensity = uncertaintyCount / Math.max(sentences.length, 1);\n if (uncertaintyDensity > 0.3) {\n riskScore += 0.2;\n riskFactors.push(`High uncertainty marker density: ${uncertaintyDensity.toFixed(2)}`);\n } else if (uncertaintyDensity > 0.15) {\n riskScore += 0.1;\n riskFactors.push(`Moderate uncertainty marker density: ${uncertaintyDensity.toFixed(2)}`);\n }\n \n // 3. Factual Grounding Indicators\n // Check for specific dates, numbers, citations, or references\n const groundingPatterns = [\n /\\b(19|20)\\d{2}\\b/g, // Years\n /\\b\\d+(\\.\\d+)?%\\b/g, // Percentages\n /\\b\\d+(\\.\\d+)?\\s*(million|billion|thousand)\\b/gi, // Large numbers\n /\\b(according to|based on|research shows|studies indicate|data suggests)\\b/gi, // Citations\n /\\b(source:|reference:|citation:)\\b/gi // Explicit references\n ];\n \n let groundingScore = 0;\n for (const pattern of groundingPatterns) {\n const matches = response.match(pattern);\n if (matches) {\n groundingScore += matches.length;\n }\n }\n \n // Low grounding in factual responses increases risk\n const groundingDensity = groundingScore / Math.max(sentences.length, 1);\n if (groundingDensity < 0.1 && response.length > 200) {\n riskScore += 0.15;\n riskFactors.push('Low factual grounding indicators');\n }\n \n // 4. Specificity vs Vagueness\n // Vague responses are more likely to be hallucinated\n const vagueTerms = [\n 'some', 'many', 'various', 'several', 'often', 'sometimes',\n 'generally', 'typically', 'usually', 'commonly', 'frequently',\n 'thing', 'stuff', 'something', 'somewhere'\n ];\n \n let vaguenessCount = 0;\n for (const term of vagueTerms) {\n const regex = new RegExp(`\\\\b${term}\\\\b`, 'gi');\n const matches = lowerResponse.match(regex);\n if (matches) {\n vaguenessCount += matches.length;\n }\n }\n \n const vaguenessDensity = vaguenessCount / Math.max(sentences.length, 1);\n if (vaguenessDensity > 0.4) {\n riskScore += 0.15;\n riskFactors.push(`High vagueness density: ${vaguenessDensity.toFixed(2)}`);\n }\n \n // 5. Response Length Analysis\n // Extremely short or verbose responses can indicate issues\n if (response.length < 50 && prompt.length > 100) {\n riskScore += 0.1;\n riskFactors.push('Response too
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
slackOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow automates end-to-end research analysis by coordinating multiple AI models—including NVIDIA NIM (Llama), OpenAI GPT-4, and Claude to analyze uploaded documents, extract insights, and generate polished reports delivered via email. Built for researchers, academics,…
Source: https://n8n.io/workflows/12537/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Advanced Workflow with Branching and Error Handling. Uses emailSend, httpRequest, postgres, slack. Webhook trigger; 12 nodes.
This n8n workflow automates task creation and scheduled reminders for users via a Telegram bot, ensuring timely notifications across multiple channels like email and Slack. It streamlines task managem
QA Platform — Jira Story to Test Workflow. Uses jiraTrigger, postgres, httpRequest, slack. Webhook trigger; 20 nodes.
This workflow provides a complete, automated post-purchase solution triggered by a successful payment webhook from Abacate Pay. (For international users, think of Abacate Pay as 'the Brazilian Stripe'
Payment Processing Workflow. Uses postgres, hubspot, emailSend, slack. Webhook trigger; 11 nodes.