This workflow corresponds to n8n.io template #16330 — we link there as the canonical source.
This workflow follows the Execute Workflow Trigger → 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": "h0ZOAW0PzyTkjUvN",
"meta": {
"builderVariant": "mcp",
"aiBuilderAssisted": true
},
"name": "Route AI prompts to Anthropic, Google Gemini, Mistral, or OpenAI",
"tags": [
{
"id": "Fur9Q2nIpUv2w23M",
"name": "triple8labs",
"createdAt": "2026-06-07T20:22:00.276Z",
"updatedAt": "2026-06-07T20:22:00.276Z"
}
],
"nodes": [
{
"id": "8367556b-c4ff-44b9-a05c-1d47478e9e91",
"name": "Sticky Note f15b7efd",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
-800
],
"parameters": {
"width": 904,
"height": 2020,
"content": "## Route AI prompts to Anthropic, Google Gemini, Mistral, or OpenAI\n\n### How it works\n\nCall this workflow from any workflow via **Execute Workflow** (sub-workflow) to route a prompt to Anthropic, Google Gemini, Mistral, or OpenAI. The response is normalised into a single `llm_response` field regardless of provider.\n\n**Minimum payload:**\n```\n{ \"userPrompt\": \"Your prompt here\" }\n```\n\n**All supported fields:**\n```\n{\n \"userPrompt\": \"...\", // required\n \"systemPrompt\": \"...\", // optional\n \"llm_provider\": \"google\", // anthropic | google | mistral | openai\n \"google_model\": \"gemini-3.5-flash\",\n \"anthropic_model\": \"claude-haiku-4-5-20251001\",\n \"mistral_model\": \"mistral-medium-latest\",\n \"openai_model\": \"gpt-5.4-mini\",\n \"temperature\": 0.5, // 0.0 - 1.0 (omitted for o-series reasoning models)\n \"max_tokens\": 5000, // integer or numeric string - both accepted\n \"response_format\": \"json\" // json | text\n}\n```\n\n**Returns** the original payload plus:\n| Field | Description |\n|---|---|\n| `llm_response` | Model text output, or `[API ERROR] ...` on failure |\n| `model_used` | Model that handled the request |\n| `provider_used` | Provider that handled the request |\n| `llm_response_length` | Character length of `llm_response` |\n| `llm_response_empty` | `true` if response is empty or an API error occurred |\n| `_input_tokens` | Input token count - actual from API if available, otherwise estimated |\n| `_output_tokens` | Output token count - actual from API if available, otherwise estimated |\n| `_estimated_cost_usd` | Estimated cost in USD. `null` if model not in pricing table |\n\n**Note on `response_format`:** For Google Gemini, `json` enforces `application/json` MIME type at the API level. For all other providers, it controls response parsing only - use your prompt to request JSON output.\n\n### Setup\n\n**1. Update the CONFIG node** with your preferred defaults:\n- `DEFAULT_PROVIDER` - which LLM to use when the caller does not specify\n- `DEFAULT_*_MODEL` - default model per provider\n- `DEFAULT_TEMPERATURE` / `DEFAULT_MAX_TOKENS` / `DEFAULT_RESPONSE_FORMAT`\n\n**2. Link credentials** for each provider you plan to use:\n- Anthropic API -> **Anthropic Chat**\n- Google AI (PaLM) API -> **Google Chat** *(the credential is named \"PaLM\" in n8n but works with current Gemini models)*\n- Mistral Cloud API -> **Mistral Chat**\n- OpenAI API -> **OpenAI Chat**\n\nCredentials for unused providers can be left unset.\n\n**3. Activate the workflow** before calling it. Inactive workflows cannot be reached via Execute Workflow.\n\n### Troubleshooting\n\n**\"userPrompt is required\"** - The calling workflow did not pass a `userPrompt` field. Field names are case-sensitive - `prompt` or `user_prompt` will not work.\n\n**\"Prompt too large\"** - Estimated token count exceeded 70% of the model's context limit. Shorten the prompt, reduce `DEFAULT_MAX_TOKENS`, or switch to a model with a larger context window.\n\n**`llm_response_empty: true`** - An API call failed. The `llm_response` field contains `[API ERROR] ...` with details. Common causes: invalid or missing credentials, rate limit (429), unsupported model name.\n\n**`_estimated_cost_usd` is null** - The model is not in the Cost Tracking node's pricing table. Add the model and its rates to the `COSTS` object in that node.\n\n### Notes\n- Prompt content is coerced to string and stripped of null bytes. Content-based filtering is the caller's responsibility.\n- Pricing in the Cost Tracking node is approximate. Verify against provider pricing pages and update the `COSTS` table as needed."
},
"typeVersion": 1
},
{
"id": "0808e3b4-68e7-4ec1-8aff-a60edd941179",
"name": "Sticky Note 6202f1fd",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
1232
],
"parameters": {
"color": 7,
"width": 448,
"height": 400,
"content": "## 1) Initialise"
},
"typeVersion": 1
},
{
"id": "389f6971-5e09-4d0b-bbc3-b8ac8e0efa6d",
"name": "Sticky Note acfb1d75",
"type": "n8n-nodes-base.stickyNote",
"position": [
464,
1232
],
"parameters": {
"color": 7,
"width": 448,
"height": 400,
"content": "## 2) Normalise & validate"
},
"typeVersion": 1
},
{
"id": "78685cb6-d14b-44d4-ae5a-32b71850e543",
"name": "Sticky Note 42d01980",
"type": "n8n-nodes-base.stickyNote",
"position": [
928,
944
],
"parameters": {
"color": 7,
"width": 688,
"height": 920,
"content": "## 3) Route to provider"
},
"typeVersion": 1
},
{
"id": "c476de12-71dc-4169-b1e1-48889e18e0ac",
"name": "Sticky Note cf57dd67",
"type": "n8n-nodes-base.stickyNote",
"position": [
1632,
1232
],
"parameters": {
"color": 7,
"width": 640,
"height": 400,
"content": "## 4) Merge & return"
},
"typeVersion": 1
},
{
"id": "12d446f9-4d1a-4d64-9737-0701b85b4d0c",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"position": [
80,
1392
],
"parameters": {
"inputSource": "passthrough"
},
"typeVersion": 1.1
},
{
"id": "fe3678aa-7a9e-4f10-a545-a414421f0186",
"name": "CONFIG",
"type": "n8n-nodes-base.set",
"position": [
304,
1392
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "def_prov",
"name": "DEFAULT_PROVIDER",
"type": "string",
"value": "google"
},
{
"id": "def_anth",
"name": "DEFAULT_ANTHROPIC_MODEL",
"type": "string",
"value": "claude-haiku-4-5-20251001"
},
{
"id": "def_goog",
"name": "DEFAULT_GOOGLE_MODEL",
"type": "string",
"value": "gemini-3.5-flash"
},
{
"id": "def_mist",
"name": "DEFAULT_MISTRAL_MODEL",
"type": "string",
"value": "mistral-medium-latest"
},
{
"id": "def_oai",
"name": "DEFAULT_OPENAI_MODEL",
"type": "string",
"value": "gpt-5.4-mini"
},
{
"id": "def_temp",
"name": "DEFAULT_TEMPERATURE",
"type": "number",
"value": 0.5
},
{
"id": "def_tok",
"name": "DEFAULT_MAX_TOKENS",
"type": "number",
"value": 5000
},
{
"id": "def_fmt",
"name": "DEFAULT_RESPONSE_FORMAT",
"type": "string",
"value": "json"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "726c3278-6dbb-4c84-9087-bc011c084fb4",
"name": "Normalize Input",
"type": "n8n-nodes-base.code",
"position": [
528,
1392
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Normalize Input\nconst triggerData = $('Execute Workflow Trigger').first().json || {};\nconst j = { ...triggerData, ...($json || {}) };\n\nif (!j.userPrompt) throw new Error('userPrompt is required');\n\nfunction sanitizePrompt(val) {\n if (val == null) return '';\n return String(val).replace(/\\x00/g, '');\n}\n\nconst llm_provider = (j.llm_provider || j.DEFAULT_PROVIDER || 'google').toLowerCase();\n\nlet model;\nif (llm_provider === 'anthropic') {\n model = j.anthropic_model || j.DEFAULT_ANTHROPIC_MODEL || 'claude-haiku-4-5-20251001';\n} else if (llm_provider === 'google') {\n model = j.google_model || j.DEFAULT_GOOGLE_MODEL || 'gemini-3.5-flash';\n} else if (llm_provider === 'mistral') {\n model = j.mistral_model || j.DEFAULT_MISTRAL_MODEL || 'mistral-medium-latest';\n} else if (llm_provider === 'openai') {\n model = j.openai_model || j.DEFAULT_OPENAI_MODEL || 'gpt-5.4-mini';\n}\n\nconst temperature = typeof j.temperature === 'number' ? j.temperature\n : typeof j.temperature === 'string' ? parseFloat(j.temperature) || 0.5\n : typeof j.DEFAULT_TEMPERATURE === 'number' ? j.DEFAULT_TEMPERATURE\n : 0.5;\n\nconst max_tokens = parseInt(j.max_tokens, 10)\n || parseInt(j.DEFAULT_MAX_TOKENS, 10)\n || 5000;\n\nconst response_format = j.response_format || j.DEFAULT_RESPONSE_FORMAT || 'json';\nconst systemPrompt = sanitizePrompt(j.systemPrompt);\nconst userPrompt = sanitizePrompt(j.userPrompt);\n\nconst isReasoningModel = llm_provider === 'openai' && /^o[0-9]/i.test(model);\nconst isNewOpenAI = llm_provider === 'openai' && /^gpt-5/i.test(model);\n\nconst provider_body = {\n model,\n ...(!isReasoningModel && { temperature }),\n ...((isNewOpenAI || isReasoningModel) ? { max_completion_tokens: max_tokens } : { max_tokens }),\n messages: [\n { role: 'system', content: systemPrompt },\n { role: 'user', content: userPrompt }\n ]\n};\n\nreturn {\n json: {\n ...j,\n llm_provider,\n response_format,\n model_used: model,\n provider_used: llm_provider,\n provider_body\n }\n};"
},
"typeVersion": 2
},
{
"id": "972a68a3-d8e0-4bf7-89ee-e682e6fe5515",
"name": "Validate Token Budget",
"type": "n8n-nodes-base.code",
"position": [
752,
1392
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "function estimateTokens(text, provider) {\n if (!text) return 0;\n const str = String(text);\n if (provider === 'openai' || provider === 'anthropic') {\n const words = str.trim() ? str.trim().split(/\\s+/).length : 0;\n return Math.ceil(words * 1.3);\n } else if (provider === 'google') {\n return Math.ceil(str.length / 3.5);\n }\n return Math.ceil(str.length / 4);\n}\n\nconst provider = ($json.llm_provider || 'google').toLowerCase();\nconst systemContent = $json.provider_body?.messages?.[0]?.content || '';\nconst userContent = $json.provider_body?.messages?.[1]?.content || '';\n\nconst systemTokens = estimateTokens(systemContent, provider);\nconst userTokens = estimateTokens(userContent, provider);\nconst totalInput = systemTokens + userTokens;\n\nconst MODEL_LIMITS = {\n 'claude-haiku-4-5-20251001': 200000,\n 'claude-haiku-4-5': 200000,\n 'claude-sonnet-4-6': 1000000,\n 'claude-sonnet-4-5-20250929': 200000,\n 'claude-sonnet-4-5': 200000,\n 'claude-opus-4-8': 1000000,\n 'claude-opus-4-7': 1000000,\n 'claude-fable-5': 1000000,\n 'gemini-3.5-flash': 1000000,\n 'gemini-3-flash': 1000000,\n 'gemini-2.5-flash': 1000000,\n 'gemini-2.5-flash-lite': 1000000,\n 'gpt-5.5': 1000000,\n 'gpt-5.4': 1000000,\n 'gpt-5.4-mini': 400000,\n 'gpt-4o': 128000,\n 'gpt-4o-mini': 128000,\n 'mistral-medium-latest': 128000,\n 'mistral-small-latest': 128000,\n 'mistral-large-latest': 128000,\n};\n\nconst model = $json.provider_body?.model || '';\nconst limit = MODEL_LIMITS[model] || 32000;\n\nif (totalInput > limit * 0.7) {\n throw new Error(\n `Prompt too large: ~${totalInput} estimated tokens (model limit: ${limit}). ` +\n `Reduce prompt length or switch to a larger model.`\n );\n}\n\nreturn { json: { ...$json, _token_estimate: totalInput } };"
},
"typeVersion": 2
},
{
"id": "24080dcb-f852-46d9-bf70-5cbf24f16852",
"name": "Which Provider?",
"type": "n8n-nodes-base.switch",
"position": [
976,
1360
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "anthropic",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "d1c1e6e7-06b7-446c-9ebc-1cbf73e7eca0",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.llm_provider }}",
"rightValue": "anthropic"
}
]
},
"renameOutput": true
},
{
"outputKey": "google",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "2252afb1-5f07-4b84-89ee-1d036e70ec3b",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.llm_provider }}",
"rightValue": "google"
}
]
},
"renameOutput": true
},
{
"outputKey": "mistral",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "36979d2f-1458-4777-b3b7-df7e0279227b",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.llm_provider }}",
"rightValue": "mistral"
}
]
},
"renameOutput": true
},
{
"outputKey": "openai",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f260d701-b399-452f-b4ad-6c60c6282a45",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.llm_provider }}",
"rightValue": "openai"
}
]
},
"renameOutput": true
}
]
},
"options": {
"ignoreCase": true
}
},
"typeVersion": 3.2
},
{
"id": "9af34dfa-c19a-4c28-9844-3e9e70a1032f",
"name": "Anthropic Chat",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
1312,
1104
],
"parameters": {
"url": "https://api.anthropic.com/v1/messages",
"method": "POST",
"options": {
"timeout": 300000,
"response": {
"response": {
"fullResponse": true
}
}
},
"jsonBody": "={{ JSON.stringify({ model: $json.provider_body.model, max_tokens: $json.provider_body.max_tokens, temperature: $json.provider_body.temperature, system: $json.provider_body?.messages?.[0]?.content || '', messages: [ { role: 'user', content: [ { type: 'text', text: $json.provider_body?.messages?.[1]?.content || '' } ] } ] }) }}",
"sendBody": true,
"jsonHeaders": "={{ JSON.stringify({ 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01' }) }}",
"sendHeaders": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"specifyHeaders": "json",
"nodeCredentialType": "anthropicApi"
},
"typeVersion": 3
},
{
"id": "8b231a4a-bda0-4c6a-9fac-013d56e8b469",
"name": "Merge Provider Response",
"type": "n8n-nodes-base.code",
"position": [
1696,
1392
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Merge Provider Response\nconst idx = typeof $itemIndex === 'number' ? $itemIndex : 0;\nconst orig = ($items('Validate Token Budget')?.[idx]?.json) || {};\n\nconst base = $json?.body ?? $json ?? {};\n\nfunction extractContentFromJSON(str) {\n if (!str) return str;\n let clean = str.replace(/^```(json)?\\n?/i, '').replace(/\\n?```$/i, '').trim();\n try {\n const parsed = JSON.parse(clean);\n if (typeof parsed === 'object' && parsed !== null) {\n if (parsed.output) return parsed.output;\n if (parsed.response) return parsed.response;\n if (parsed.content) return parsed.content;\n if (parsed.story) return parsed.story;\n if (parsed.text) return parsed.text;\n if (parsed.story_bible) return parsed.story_bible;\n return clean;\n }\n return clean;\n } catch (e) {\n clean = clean.replace(/(?<!:)(\\s)\"([a-zA-Z0-9])/g, '$1\\\\\"$2');\n clean = clean.replace(/([a-zA-Z0-9\\.?!])\"(?![,\\}\\]:])/g, '$1\\\\\"');\n return clean;\n }\n}\n\nlet outputText = '';\nlet rawErrorBody = '';\n\nif (base.candidates?.[0]?.content?.parts?.[0]?.text) {\n outputText = base.candidates[0].content.parts[0].text;\n}\nelse if (Array.isArray(base.content) && base.content[0]?.text) {\n outputText = base.content[0].text;\n}\nelse if (base.choices?.[0]?.message?.content) {\n outputText = base.choices[0].message.content;\n}\nelse if (base.text) outputText = base.text;\nelse if (base.content && typeof base.content === 'string') outputText = base.content;\nelse if (base.message?.content) outputText = base.message.content;\nelse if (base.output) outputText = base.output;\nelse if (base.error) {\n rawErrorBody = '[API ERROR] ' + JSON.stringify(base.error);\n} else {\n const raw = JSON.stringify($json);\n if (raw && raw.length > 2) rawErrorBody = '[UNEXPECTED RESPONSE] ' + raw.substring(0, 300);\n}\n\nconst trimmed = outputText ? outputText.trim() : '';\nconst isJSON = trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.includes('```json');\nconst callerWantsJson = (orig.response_format || '').toLowerCase().includes('json');\n\nif (isJSON) {\n if (callerWantsJson) {\n outputText = trimmed.replace(/^```(json)?\\n?/i, '').replace(/\\n?```$/i, '').trim();\n } else {\n outputText = extractContentFromJSON(outputText);\n }\n}\n\nlet _actual_input_tokens = null;\nlet _actual_output_tokens = null;\n\nif (base.usage) {\n if (typeof base.usage.input_tokens === 'number') {\n _actual_input_tokens = base.usage.input_tokens;\n _actual_output_tokens = base.usage.output_tokens ?? null;\n }\n else if (typeof base.usage.prompt_tokens === 'number') {\n _actual_input_tokens = base.usage.prompt_tokens;\n _actual_output_tokens = base.usage.completion_tokens ?? null;\n }\n}\nelse if (base.usageMetadata) {\n _actual_input_tokens = base.usageMetadata.promptTokenCount ?? null;\n _actual_output_tokens = base.usageMetadata.candidatesTokenCount ?? null;\n}\n\ndelete orig.provider_body;\n\nreturn {\n json: {\n ...orig,\n llm_response: outputText || rawErrorBody,\n _actual_input_tokens,\n _actual_output_tokens,\n }\n};"
},
"typeVersion": 2
},
{
"id": "e39b37a7-7926-404d-a120-b242e9dfd133",
"name": "Google Chat",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
1312,
1296
],
"parameters": {
"url": "=https://generativelanguage.googleapis.com/v1beta/models/{{$json.provider_body.model}}:generateContent",
"body": "={{ JSON.stringify({ \"contents\": [{ \"role\": \"user\", \"parts\": [{ \"text\": $json.provider_body?.messages?.[1]?.content || \"\" }] }], \"systemInstruction\": { \"parts\": [{ \"text\": $json.provider_body?.messages?.[0]?.content || \"\" }] }, \"generationConfig\": { \"temperature\": $json.provider_body.temperature, \"maxOutputTokens\": $json.provider_body.max_tokens, \"responseMimeType\": ($json.response_format || \"\").toLowerCase().includes(\"json\") ? \"application/json\" : \"text/plain\" } }) }}",
"method": "POST",
"options": {
"timeout": 300000,
"response": {
"response": {
"fullResponse": true
}
}
},
"sendBody": true,
"contentType": "raw",
"jsonHeaders": "={{ JSON.stringify({ 'Content-Type': 'application/json' }) }}",
"sendHeaders": true,
"authentication": "predefinedCredentialType",
"rawContentType": "application/json",
"specifyHeaders": "json",
"nodeCredentialType": "googlePalmApi"
},
"typeVersion": 4.3
},
{
"id": "001aefdf-2257-4c00-95dd-26277bf9bb6a",
"name": "Mistral Chat",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
1312,
1488
],
"parameters": {
"url": "https://api.mistral.ai/v1/chat/completions",
"method": "POST",
"options": {
"timeout": 300000,
"response": {
"response": {
"fullResponse": true
}
}
},
"jsonBody": "={{ $json.provider_body }}",
"sendBody": true,
"jsonHeaders": "={{ JSON.stringify({ 'Content-Type': 'application/json' }) }}",
"sendHeaders": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"specifyHeaders": "json",
"nodeCredentialType": "mistralCloudApi"
},
"typeVersion": 3
},
{
"id": "38377ebd-df47-4c47-bfec-f51e94b25242",
"name": "OpenAI Chat",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
1312,
1680
],
"parameters": {
"url": "https://api.openai.com/v1/chat/completions",
"method": "POST",
"options": {
"timeout": 300000,
"response": {
"response": {
"fullResponse": true
}
}
},
"jsonBody": "={{ $json.provider_body }}",
"sendBody": true,
"jsonHeaders": "={{ JSON.stringify({ 'Content-Type': 'application/json' }) }}",
"sendHeaders": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"specifyHeaders": "json",
"nodeCredentialType": "openAiApi"
},
"typeVersion": 3
},
{
"id": "cbf91aba-8388-4e9f-8fd6-a1d1fe24c1dc",
"name": "Cost Tracking",
"type": "n8n-nodes-base.code",
"position": [
1904,
1392
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const COSTS = {\n anthropic: {\n 'claude-haiku-4-5-20251001': { input: 0.001, output: 0.005 },\n 'claude-haiku-4-5': { input: 0.001, output: 0.005 },\n 'claude-sonnet-4-6': { input: 0.003, output: 0.015 },\n 'claude-sonnet-4-5-20250929': { input: 0.003, output: 0.015 },\n 'claude-sonnet-4-5': { input: 0.003, output: 0.015 },\n 'claude-opus-4-8': { input: 0.005, output: 0.025 },\n 'claude-opus-4-7': { input: 0.005, output: 0.025 },\n 'claude-fable-5': { input: 0.01, output: 0.05 },\n },\n google: {\n 'gemini-3.5-flash': { input: 0.0003, output: 0.0025 },\n 'gemini-3-flash': { input: 0.001, output: 0.004 },\n 'gemini-2.5-flash': { input: 0.0003, output: 0.0025 },\n 'gemini-2.5-flash-lite': { input: 0.0001, output: 0.0004 },\n },\n openai: {\n 'gpt-5.5': { input: 0.005, output: 0.02 },\n 'gpt-5.4': { input: 0.005, output: 0.02 },\n 'gpt-5.4-mini': { input: 0.0003, output: 0.0012 },\n 'gpt-4o': { input: 0.0025, output: 0.01 },\n 'gpt-4o-mini': { input: 0.00015, output: 0.0006 },\n },\n mistral: {\n 'mistral-medium-latest': { input: 0.0004, output: 0.002 },\n 'mistral-small-latest': { input: 0.0001, output: 0.0003 },\n 'mistral-large-latest': { input: 0.002, output: 0.006 },\n },\n};\n\nconst provider = $json.provider_used || '';\nconst model = $json.model_used || '';\nconst llmResponse = $json.llm_response || '';\n\nconst inputTokens = $json._actual_input_tokens\n ?? ($json._token_estimate || 0);\nconst outputTokens = $json._actual_output_tokens\n ?? Math.ceil(llmResponse.length / 4);\n\nconst rates = COSTS[provider]?.[model];\nconst estimatedCostUsd = rates\n ? (inputTokens / 1000) * rates.input + (outputTokens / 1000) * rates.output\n : null;\n\nreturn {\n json: {\n ...$json,\n llm_response_length: llmResponse.length,\n llm_response_empty: llmResponse.length === 0,\n _input_tokens: inputTokens,\n _output_tokens: outputTokens,\n _estimated_cost_usd: estimatedCostUsd,\n }\n};"
},
"typeVersion": 2
},
{
"id": "7b8431ae-0ec1-4932-ac62-0378b7317326",
"name": "Return",
"type": "n8n-nodes-base.code",
"position": [
2080,
1392
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const idx = typeof $itemIndex === 'number' ? $itemIndex : 0;\nconst orig = ($items('Execute Workflow Trigger')?.[idx]?.json) || {};\n\nreturn { json: { ...orig, ...$json } };"
},
"typeVersion": 2
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": true,
"executionOrder": "v1"
},
"versionId": "fc9e2858-641d-4dfc-96b9-fc5107ea53f0",
"nodeGroups": [],
"connections": {
"CONFIG": {
"main": [
[
{
"node": "Normalize Input",
"type": "main",
"index": 0
}
]
]
},
"Google Chat": {
"main": [
[
{
"node": "Merge Provider Response",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Chat": {
"main": [
[
{
"node": "Merge Provider Response",
"type": "main",
"index": 0
}
]
]
},
"Mistral Chat": {
"main": [
[
{
"node": "Merge Provider Response",
"type": "main",
"index": 0
}
]
]
},
"Cost Tracking": {
"main": [
[
{
"node": "Return",
"type": "main",
"index": 0
}
]
]
},
"Anthropic Chat": {
"main": [
[
{
"node": "Merge Provider Response",
"type": "main",
"index": 0
}
]
]
},
"Normalize Input": {
"main": [
[
{
"node": "Validate Token Budget",
"type": "main",
"index": 0
}
]
]
},
"Which Provider?": {
"main": [
[
{
"node": "Anthropic Chat",
"type": "main",
"index": 0
}
],
[
{
"node": "Google Chat",
"type": "main",
"index": 0
}
],
[
{
"node": "Mistral Chat",
"type": "main",
"index": 0
}
],
[
{
"node": "OpenAI Chat",
"type": "main",
"index": 0
}
]
]
},
"Validate Token Budget": {
"main": [
[
{
"node": "Which Provider?",
"type": "main",
"index": 0
}
]
]
},
"Merge Provider Response": {
"main": [
[
{
"node": "Cost Tracking",
"type": "main",
"index": 0
}
]
]
},
"Execute Workflow Trigger": {
"main": [
[
{
"node": "CONFIG",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This sub-workflow is called via Execute Workflow and routes a prompt to Anthropic, Google Gemini, Mistral, or OpenAI, then normalizes the model output into a single response field while estimating token usage and cost. Receives input from another workflow via the Execute…
Source: https://n8n.io/workflows/16330/ — 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.
[2/2] KNN classifier (lands dataset). Uses httpRequest, stickyNote, executeWorkflowTrigger. Event-driven trigger; 18 nodes.
Workflows from the webinar "Build production-ready AI Agents with Qdrant and n8n".
[3/3] Anomaly detection tool (crops dataset). Uses stickyNote, httpRequest, executeWorkflowTrigger. Event-driven trigger; 17 nodes.
Workflows from the webinar "Build production-ready AI Agents with Qdrant and n8n".
works with selfhosted Supabase