{
  "id": "39pmihe4FUZNNFnQ",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "LLM Cost Tracking",
  "tags": [],
  "nodes": [
    {
      "id": "7e87063c-72a7-4c33-a5d4-9f5ef550da21",
      "name": "Get an execution",
      "type": "n8n-nodes-base.n8n",
      "position": [
        -688,
        320
      ],
      "parameters": {
        "options": {
          "activeWorkflows": true
        },
        "resource": "execution",
        "operation": "get",
        "executionId": "={{ $json.execution_id }}",
        "requestOptions": {}
      },
      "credentials": {
        "n8nApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "fdc6c95b-361d-445b-8306-1712e7235eaf",
      "name": "When Exc.",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        -880,
        416
      ],
      "parameters": {
        "workflowInputs": {
          "values": [
            {
              "name": "execution_id",
              "type": "number"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "bce95431-c1be-47bc-96f3-99b318e78af8",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -240,
        -80
      ],
      "parameters": {
        "width": 352,
        "height": 304,
        "content": "### Defined by User\n- Define the model name and standard name, even if you want to use same name. (Why? because we will use the standard name to find the cost of this model)\n- Model prices are per million token\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "a1f0416a-723c-4724-a8f5-7ff3dbb6b829",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        160,
        128
      ],
      "parameters": {
        "color": 3,
        "width": 416,
        "height": 240,
        "content": "### Check all model names are correctly defined"
      },
      "typeVersion": 1
    },
    {
      "id": "e55084c1-e2b6-4788-9f6e-1cfaeef59165",
      "name": "model prices",
      "type": "n8n-nodes-base.set",
      "position": [
        -32,
        96
      ],
      "parameters": {
        "mode": "raw",
        "include": "selected",
        "options": {},
        "jsonOutput": "{\n  \"model_price_dic\":{\n      \"gpt-5\": { \"input\": 1.25, \"output\": 10.0 },\n      \"gpt-5-mini\": { \"input\": 0.25,  \"output\": 2.0 },\n      \"gpt-5-nano\": { \"input\": 0.05, \"output\": 0.4 },\n      \"gpt-4.1\": { \"input\": 2.0, \"output\": 8.0 },\n      \"gpt-4.1-mini\": { \"input\": 0.4, \"output\": 1.6 },\n      \"gpt-4.1-nano\": { \"input\": 0.1,  \"output\": 0.4 },\n      \"gpt-4o\": { \"input\": 2.5,  \"output\": 10.0 },\n      \"gpt-4o-mini\": { \"input\": 0.15, \"output\": 0.6 },\n      \"o1\": { \"input\": 15.0, \"output\": 60.0 },\n      \"o1-pro\": { \"input\": 150.0,  \"output\": 600.0 },\n      \"o3-pro\": { \"input\": 20.0,  \"output\": 80.0 },\n      \"o3\": { \"input\": 2.0,  \"output\": 8.0 },\n      \"o3-deep-research\": { \"input\": 10.0,\"output\": 40.0 },\n      \"o4-mini\": { \"input\": 1.1,  \"output\": 4.4 },\n      \"o4-mini-deep-research\": { \"input\": 2.0,\"output\": 8.0 },\n      \"o3-mini\": { \"input\": 1.1, \"output\": 4.4 },\n      \"o1-mini\": { \"input\": 1.1,  \"output\": 4.4 }\n    }\n  }",
        "includeFields": "standardize_names_dic, models_used",
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "19971fb6-2456-4629-ba2b-8bcacbeda97f",
      "name": "Standardize names",
      "type": "n8n-nodes-base.set",
      "position": [
        -208,
        96
      ],
      "parameters": {
        "mode": "raw",
        "include": "selected",
        "options": {},
        "jsonOutput": "{\n  \"standardize_names_dic\":\n    {\n    \"gpt-4.1-mini\": \"gpt-4.1-mini\",\n    \"gpt-4\": \"gpt-4\",\n    \"gpt-5\": \"gpt-5\",\n    \"gpt-5-mini\": \"gpt-5-mini\",\n    \"gpt-5-nano\": \"gpt-5-nano\"\n    }\n}\n",
        "includeFields": "models_used",
        "includeOtherFields": true
      },
      "typeVersion": 3.4
    },
    {
      "id": "fe9bde19-e1d5-40a9-9219-d79668d7a8c1",
      "name": "Check correctly defined",
      "type": "n8n-nodes-base.code",
      "position": [
        640,
        192
      ],
      "parameters": {
        "jsCode": "// NODE 2: Validate Models Against Dictionaries\n\nconst item = $input.first().json;\nconst models_used = item.models_used || [];\nconst standardize_names_dic = item.standardize_names_dic || {};\nconst model_price_dic = item.model_price_dic || {};\n\n// Check which models are missing from each dictionary\nconst missing_from_names = models_used.filter(model => !standardize_names_dic[model]);\nconst missing_from_prices = models_used.filter(model => !model_price_dic[model]);\n\n// Build result\nif (missing_from_names.length === 0 && missing_from_prices.length === 0) {\n  return [{\n    json: {\n      passed: true,\n      message: \"All models validated successfully\",\n      models_used: models_used,\n      standardize_names_dic: standardize_names_dic,\n      model_price_dic: model_price_dic,\n      original_data: item.original_data\n    }\n  }];\n} else {\n  const errors = [];\n  if (missing_from_names.length > 0) {\n    errors.push(`Missing from standardize_names_dic: ${missing_from_names.join(', ')}`);\n  }\n  if (missing_from_prices.length > 0) {\n    errors.push(`Missing from model_price_dic: ${missing_from_prices.join(', ')}`);\n  }\n  \n  return [{\n    json: {\n      passed: false,\n      message: errors.join(' | '),\n      missing_from_names: missing_from_names,\n      missing_from_prices: missing_from_prices,\n      models_used: models_used\n    }\n  }];\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "57480286-a52e-4e57-a85f-b588b8b3c74e",
      "name": "Stop and Error",
      "type": "n8n-nodes-base.stopAndError",
      "position": [
        1024,
        144
      ],
      "parameters": {
        "errorMessage": "={{ $json.message }}\n\nThings missed from \"standardized names\":\n{{ $json.missing_from_names?.join(', ') || \"none\" }}\n\nThings missed from \"model price\":\n{{ $json.missing_from_prices?.join(', ') || \"none\" }}"
      },
      "typeVersion": 1
    },
    {
      "id": "fbfa0f9b-ae78-4051-a878-a4ef4861c057",
      "name": "If not passed",
      "type": "n8n-nodes-base.if",
      "position": [
        832,
        192
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f0b929f5-c865-4380-b0d8-ca23ab3b3674",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              },
              "leftValue": "={{ $json.passed }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "e1692a9d-fcb3-431b-aa32-17fdb888e749",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        1024,
        368
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "884153b3-4920-45e3-9266-f6f151e96f6f",
      "name": "Calculate cost",
      "type": "n8n-nodes-base.code",
      "position": [
        1216,
        368
      ],
      "parameters": {
        "jsCode": "const inputData = $input.all();\n\n// Get the two items from input\nconst item1 = inputData[0].json;\nconst item2 = inputData[1]?.json || {};\n\n// Merge the data from both items\nconst data = {...item1, ...item2};\n\n// Get the components\nconst llm_run_data = data.llm_run_data || {};\nconst tokensByModel = data.tokensByModel || {};\nconst model_price_dic = data.model_price_dic || {};\n\n// Deep copy and add costs to llm_run_data\nconst updated_llm_run_data = {};\nObject.keys(llm_run_data).forEach(key => {\n  const item = JSON.parse(JSON.stringify(llm_run_data[key]));\n  const model = item.options?.model || item.model || 'unknown';\n  const prices = model_price_dic[model];\n  \n  if (prices && item.tokenUsage) {\n    const promptTokens = item.tokenUsage.promptTokens || 0;\n    const completionTokens = item.tokenUsage.completionTokens || 0;\n    \n    // Calculate costs (price is per million tokens)\n    const input_cost = (promptTokens / 1000000) * prices.input;\n    const output_cost = (completionTokens / 1000000) * prices.output;\n    \n    item.tokenUsage_cost = {\n      input_cost: input_cost,\n      output_cost: output_cost,\n      total_cost: input_cost + output_cost\n    };\n  } else {\n    item.tokenUsage_cost = {\n      input_cost: 0,\n      output_cost: 0,\n      total_cost: 0\n    };\n  }\n  updated_llm_run_data[key] = item;\n});\n\n// Deep copy and add costs to tokensByModel\nconst updated_tokensByModel = {};\nObject.keys(tokensByModel).forEach(model => {\n  const modelData = JSON.parse(JSON.stringify(tokensByModel[model]));\n  const prices = model_price_dic[model];\n  \n  if (prices) {\n    const promptTokens = modelData.promptTokens || 0;\n    const completionTokens = modelData.completionTokens || 0;\n    \n    // Calculate costs (price is per million tokens)\n    const input_cost = (promptTokens / 1000000) * prices.input;\n    const output_cost = (completionTokens / 1000000) * prices.output;\n    \n    modelData.input_cost = input_cost;\n    modelData.output_cost = output_cost;\n    modelData.total_cost = input_cost + output_cost;\n  } else {\n    modelData.input_cost = 0;\n    modelData.output_cost = 0;\n    modelData.total_cost = 0;\n  }\n  updated_tokensByModel[model] = modelData;\n});\n\n// Calculate total costs across all models\nlet total_input_cost = 0;\nlet total_output_cost = 0;\n\nObject.values(updated_tokensByModel).forEach(modelData => {\n  total_input_cost += modelData.input_cost || 0;\n  total_output_cost += modelData.output_cost || 0;\n});\n\nconst cost = {\n  input_cost: total_input_cost,\n  output_cost: total_output_cost,\n  total_cost: total_input_cost + total_output_cost\n};\n\n// Return merged data with updates\nreturn [{\n  json: {\n    ...data,\n    llm_run_data: updated_llm_run_data,\n    tokensByModel: updated_tokensByModel,\n    cost: cost\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "cfb7c844-4921-4d27-828d-e5792e457943",
      "name": "Test id",
      "type": "n8n-nodes-base.manualTrigger",
      "notes": "283\n353",
      "position": [
        -880,
        208
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "765452b4-d9d7-463d-b465-11296af1fc73",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1136,
        80
      ],
      "parameters": {
        "color": 7,
        "width": 368,
        "height": 240,
        "content": "### Test with execution id\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "102aa26e-b208-44f2-a4a8-11a56046b546",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1120,
        144
      ],
      "parameters": {
        "color": 7,
        "width": 214,
        "content": "Where is the execution ID?\nIn your workflow at the top center, you can see \"Executions,\" and you can select any execution to see the ID, which is a number like 323.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "eab0b1f1-3cb4-4b0b-913e-6c5639b17b26",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        784,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 214,
        "height": 80,
        "content": "In case you did something incorrectly, you can see what models you missed to add and define"
      },
      "typeVersion": 1
    },
    {
      "id": "e275b913-ea04-4c43-b799-d5fa4f31ee65",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1456,
        352
      ],
      "parameters": {
        "color": 7,
        "width": 246,
        "height": 144,
        "content": "You can do anything with this info:\n- Send a follow-up message with cost\n- Send another request to continue a process ..."
      },
      "typeVersion": 1
    },
    {
      "id": "481d382a-cfc8-47cc-9ac2-89a9c3118419",
      "name": "Find Nodes with LLM Use",
      "type": "n8n-nodes-base.code",
      "position": [
        -400,
        384
      ],
      "parameters": {
        "jsCode": "/**\n * n8n JavaScript Code - Simple Version\n * Extracts node execution data and LLM run data from agentic framework\n */\n\n// Get the input data - adjust this based on your n8n node configuration\nconst inputData = $input.first().json;\n\n// Navigate to runData - adjust path if needed\nconst runData = inputData.resultData?.runData || \n                inputData.data?.resultData?.runData || \n                inputData.runData;\n\nif (!runData) {\n  return [{\n    json: {\n      error: \"Could not find runData in input\",\n      hint: \"Check if path to runData is correct\"\n    }\n  }];\n}\n\n// Initialize output structures\nconst all_nodes_summary = [];\nconst llm_run_data = {};\n\n// Process each node\nObject.keys(runData).forEach(nodeName => {\n  const nodeExecutions = runData[nodeName];\n  \n  // Process each execution in the node's array\n  nodeExecutions.forEach((execution, idx) => {\n    // Store node execution summary\n    all_nodes_summary.push({\n      nodeName: nodeName,\n      arrayIndex: idx,\n      startTime: execution.startTime,\n      executionTime: execution.executionTime,\n      executionIndex: execution.executionIndex,\n      executionStatus: execution.executionStatus\n    });\n    \n    // Helper function to find ai_languageModel at level 0 and 1\n    function findAILanguageModel(obj, depth = 0) {\n      const found = [];\n      \n      if (!obj || depth > 1) return found;\n      \n      // Check current level\n      if (obj.ai_languageModel) {\n        found.push({ path: 'level_' + depth, data: obj.ai_languageModel });\n      }\n      \n      // Check next level if at depth 0\n      if (depth === 0) {\n        Object.keys(obj).forEach(key => {\n          if (key !== 'ai_languageModel' && typeof obj[key] === 'object' && obj[key]) {\n            if (obj[key].ai_languageModel) {\n              found.push({ path: key, data: obj[key].ai_languageModel });\n            }\n          }\n        });\n      }\n      \n      return found;\n    }\n    \n    // Look for ai_languageModel in data\n    if (execution.data) {\n      const aiModels = findAILanguageModel(execution.data);\n      \n      aiModels.forEach(model => {\n        // ai_languageModel is list of lists\n        model.data.forEach((outerList, i) => {\n          if (Array.isArray(outerList)) {\n            outerList.forEach((item, ii) => {\n              if (item?.json) {\n                const key = `llm_${nodeName}_${idx}_${i}_${ii}`;\n                // Store the entire json object\n                llm_run_data[key] = item.json;\n              }\n            });\n          }\n        });\n      });\n    }\n    \n    // Look for ai_languageModel in inputOverride\n    if (execution.inputOverride) {\n      const aiModels = findAILanguageModel(execution.inputOverride);\n      \n      aiModels.forEach(model => {\n        // ai_languageModel is list of lists\n        model.data.forEach((outerList, i) => {\n          if (Array.isArray(outerList)) {\n            outerList.forEach((item, ii) => {\n              if (item?.json) {\n                const key = `llm_${nodeName}_${idx}_${i}_${ii}_inputOverride`;\n                \n                // Check if we already have this from data\n                const dataKey = `llm_${nodeName}_${idx}_${i}_${ii}`;\n                if (llm_run_data[dataKey]) {\n                  // Merge options from inputOverride with data\n                  llm_run_data[dataKey] = {\n                    ...llm_run_data[dataKey],\n                    ...item.json\n                  };\n                } else {\n                  // Add as new entry\n                  llm_run_data[key] = item.json;\n                }\n              }\n            });\n          }\n        });\n      });\n    }\n  });\n});\n\n// Calculate tokensByModel\nconst tokensByModel = {};\n\nObject.values(llm_run_data).forEach(item => {\n  // item is the complete json object from ai_languageModel\n  // Get model from options.model or directly from item\n  const model = item.options?.model || item.model || 'unknown';\n  \n  if (!tokensByModel[model]) {\n    tokensByModel[model] = {\n      count: 0,\n      estimatedTokens: 0,\n      actualTokens: 0,\n      completionTokens: 0,\n      promptTokens: 0\n    };\n  }\n  \n  tokensByModel[model].count++;\n  tokensByModel[model].estimatedTokens += item.estimatedTokens || 0;\n  \n  // Read tokenUsage correctly from json.tokenUsage dictionary\n  // Structure: json.tokenUsage.totalTokens, json.tokenUsage.completionTokens, json.tokenUsage.promptTokens\n  if (item.tokenUsage && typeof item.tokenUsage === 'object') {\n    tokensByModel[model].actualTokens += item.tokenUsage.totalTokens || 0;\n    tokensByModel[model].completionTokens += item.tokenUsage.completionTokens || 0;\n    tokensByModel[model].promptTokens += item.tokenUsage.promptTokens || 0;\n  }\n});\n\n// Return the extracted data\nreturn [{\n  json: {\n    all_nodes_summary: all_nodes_summary,\n    llm_run_data: llm_run_data,\n    tokensByModel: tokensByModel\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "86e71222-0e8c-4b07-9b10-63de24aa5335",
      "name": "create models_used list",
      "type": "n8n-nodes-base.code",
      "position": [
        -416,
        144
      ],
      "parameters": {
        "jsCode": "const inputData = $input.first().json;\n\n// Handle array input\nconst data = Array.isArray(inputData) ? inputData[0] : inputData;\n\n// Get model names from tokensByModel\nconst models_used = data.tokensByModel ? Object.keys(data.tokensByModel) : [];\n\nreturn [{\n  json: {\n    models_used: models_used\n  }\n}];"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "f429a378-92e1-46e9-a140-5405f3c49888",
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "Calculate cost",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Test id": {
      "main": [
        [
          {
            "node": "Get an execution",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Exc.": {
      "main": [
        [
          {
            "node": "Get an execution",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "model prices": {
      "main": [
        [
          {
            "node": "Check correctly defined",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If not passed": {
      "main": [
        [
          {
            "node": "Stop and Error",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get an execution": {
      "main": [
        [
          {
            "node": "Find Nodes with LLM Use",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Standardize names": {
      "main": [
        [
          {
            "node": "model prices",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check correctly defined": {
      "main": [
        [
          {
            "node": "If not passed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Nodes with LLM Use": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          },
          {
            "node": "create models_used list",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "create models_used list": {
      "main": [
        [
          {
            "node": "Standardize names",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}