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.
Automação de Chargeback - Contestação SaaS. Uses httpRequest, postgres, emailSend, slack. Webhook trigger; 14 nodes.
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'