{
    "version": "https://jsonfeed.org/version/1",
    "title": "WiLine Edge Cloud — AI Tutorials",
    "home_page_url": "https://development-wec.wiline.com/docs/tutorials/",
    "description": "Tested, reproducible tutorials for self-hosting AI infrastructure on WiLine.",
    "items": [
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__authentik\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABCoAAACwCAYAAADJ54/8AAAliElEQVR4nO3dS24bSbvm8acSnksH6LlYG0jxW4HT8wTMGp2h6BWYnjdgegVFr8DUCj4ayMGZFbWCohLoYeNQsx400NQGsnqQLy1a1oWZzIjIy/8HCC67xIjXppiXJ+Py2z///CO4V6TxWNJ54DLws12U5ZvQRQAAAAAAHvxGUNEcCyMOv0aSLkLVg0ruJG0lrSVtJG2iLN+GKwcAAAAAhomg4kRFGk8k7b/OQtaCxt1JWklaR1m+ClsKAAAAAAwDQUUNNnJiJsKJIbmXtJS0YKQFAAAAALhDUFFBkcaJpLmkt2ErQWDfVQYW69CFAAAAAEDfEFQcgYACz7iRNGWEBQAAAAA0h6DiBUUan6sc7v8+bCVoua+S5lGW70IXAgAAAABdR1DxjCKNp5IWYg0KHOdO5eiKdehCAAAAAKDLCCoeYRQFTvQ1yvJZ6CIAAAAAoKsIKg7Ybh5LSZdhK0HH3UiaMBUEAAAAAKojqDAWUqzFVA8041blVJBN6EIAAAAAoEui0AW0ga1HsRYhBZpzKWltARgAAAAA4EiDH1FhIcW30HWgt+4lJYysAAAAAIDjDDqoYLoHPCGsAAAAAIAjDXbqByEFPDqTtLQdZQAAAAAALxhkUHGwBSkhBXy5lLQKXQQAAAAAtN0ggwqxBSnCeFuk8Tx0EQAAAADQZoNbo6JI45mkP0PXgUF7F2X5OnQRAAAAANBGgwoqijQeSdqIKR8I607SOMryXehCAAAAAKBthjb1YyFCCoR3IWkWuggAAAAAaKPBjKgo0jiR9FfoOoADv0dZvg1dBAAAAAC0yZBGVCxDFwA8Mg9dAAAAAAC0zSCCiiKNpyqH2wNtcmXrpgAAAAAAzCCCCrEeANprHroAAAAAAGiT3q9RwdoU6ADWqgAAAAAAM4QRFdPQBQCvmIYuAAAAAADaotdBRZHG55KuQtcBvGIaugAAAAAAaIs3oQtwbBK6AOAIF0Uaj6Ms34QuBAAAAAjNHjiPH/3xJsrynfdiEARBBdAOU7HoKwAAAAbK1hacSkr0zI6NRRrfSVpJWrDGW7/1ejHNIo13ks5C1wEc4TbK8nHoIgAAAACfijQeS1pIelvxpdeS5gQW/dTboILdPtBB/8FwNgB9V6TxWtUvRl9zE2V50nCbAADHijSeSfrzhCbuJU2jLF81UlBNRRrPJX1uut0oy39rus2u6PPUj3HoAoCKxpLWgWsAEFCRxlNJo6bbjbJ83nSbAAC37MFr4qDpZRtGIRRpvNTpGx+cSfp3kcYfoixfnlwUWoOgAmiPRAQVwNBN1fxoA0maO2gTAOBWIgdP6VVeb24dtHu0Io0XanZ3xm9FGouwoj/6vD3pKHQBQEXj0AUAAAAALhVpPJH00UHT32y9C/RAn4MKF0+kAJfOQxcAAAAAuGLbji4dduGybXjU56AC6Jpx6AIAAAAAh2Zyuyvjpa33hI7rZVDBkB90FFvpAgAAoM9mPekDjvUyqBBD6AEAAACgNWxtCh8P5i55cN19fQ0qAAAAAADtMfbYV+KxLzhAUAEAAAAAcC3x2NfYY19wgKACAAAAANAno9AF4DQEFQAAAAAAoDUIKgAAAAAAQGsQVAAAAAAAgNZ4E7oANOpa0vaI7/tc4/XPvebLM38+knRV8TWHEklvj/g+AAAAAO23lr/r+7WnfuAIQUW/LKMsX7/2TUUaPxc6PPv6514TZfn8me9P9ExQ8dxrHr1+LoIKAAAAoC82Pe0LDjD1AwAAAADg2rqnfcEBggoAAAAAgFNRlu9UTjV37dr6QocRVAAAAAAAfJj3pA84RlABAAAAAHAuyvKtjltYv66v1gc6jqACAAAAAOCFLax/66DpWzGaojcIKgAAAAAAPiVqNqy4lZSwNkV/EFQAAAAAALyxQCFRM2EFIUUPEVQAAAAAALyKsnwXZflY0tcTmvkqQopeIqgAAAAAAAQRZflM0u+qtnXpd0nvoiyfEVL005vQBQAAAAAAhst26pgWaTyTNJE0Ujk15NDGvtbs7NF/BBUAAAAAgOBsdMQycBloAaZ+AAAAAACA1iCoAAAAAAAArUFQAQAAAAAAWoM1KnCsd6ELwHAVaZy89P+jLF/7qQTAkBRpfC5p/Nz/59jj12vvxx7vS7e98j7voizfeCsGQDAEFTgKJ334UKTxWOUKz2OVqz2/PfJ1+/+8EStCA6ihSOORHo4/Yx1x/LFjz73smKOHY8+u+QqHw84Fh18jSRcVXr//zztJW5Xvy1bShuuZdjn43CUq3+vLI14jPby3a5WfubWL+gCEQ1ABIKgijacqL1Amks5ObO6tDm4uijS+k7SStOQJDIDH7CZpImmqI26QnnGmX4893yWtoixfnlTgQNgT9Il9JTr9XLB3YV+H741Uhtprle/RpqG+cCT73M1Uvt9HB1CPHL63n4s0vtfD+X59ao0AwnMaVDyRiMt+PTwB3UjaqUy7OWEAA2AXKXM1E0685ELSR0kfLbRYqLyI2R3zYpty8peDur5EWT530O4PRRr/46DZmyjLEwftHqVI47mkzw6afufywrZI47WOHB3ksIaqPw9B32vX7LM9k/TeURfvJb0v0nih8rizYJTFryyonsjd+/CcfbD0+SDQXvR5FJ6r41CU5b9VqGGi8nPn4nh4JulK0pW9p/O2BoUOz2VV/HUw8ugoVd7rY/TxOqVP7D5+LbfX6Yf+iLJ8dfgHjQcVdhDafx3zF9sfrN7rIRFdqucnDGCI7OZgrjA3bReS/pQ03988cOMADEuAY9CZyhuSWZHG8yjLF576bS0bPTGzL18XwC85DLRvVIbZy7Al9Yt97haqP2qpqgtJ3ywQmD2++QHwMnuguJa/Y/SHpz6njez6UaTxqEjjRZHGO0n/Vplo1v2Lnak8Yfx3kcZL+4cC0GF2jFiqHJ0Q9MmyHm4ctkUazwLXAsCDIo3H9kQ51DHoTNKfRRpv7CnV4BRpfG43jluVx+A2hBSPvVV5g7u10R44gZ371yo/d75CikMXkv5dpPGa+wngOBYmr+TvGP3puXD4pBEVB8O3r05p5wX7IVxfVQ7h2jnqZ1CaHroFvMTCgLnad1G6v3GYSpoy7QzoH7vgmqt8ANIGl5L+LtL405BGV7T4PPCcwyfyU9Y8qK4l0xv23kraFGk8ZXQF8Dw7Z67lL1i8fulcWHtEhR2ANnIXUhz6qPLpZ+KhLwANOHiS8qfafXG6v3GYhS4EQHPsmmGj9oQUh/60UWa9ZiNZNmr/eeA5Fyrn8q/sAh6vOHjP2xJS7J2pHF0xD10I0GIL+Q0ppi99Q+Wg4tEByOdJ50zlyWLhsU8ANdhaNRuFn+ZRxZ9cjAL9YNcKf6n+jgI+XNmQ9PPQhbhgN4R/K8yQ/6a9V/nAbBK6kDazEYprtfs9/zyEkBCoyj4XPgYgSNLtayGFVDGosANQ6JPOxz6f2IGus4vTf6ubT8/eS1oPdQ450HU2kmujdo6ieMpblcec89CFNMXWolirfU/UT7V/Ir/s0/vVBHvPl5K+qRvn/ivCCuCBXbt7CylUbkP9qqODioMDUBvsT+zj0IUAeGDHia5fnF6K4wvQOfaZ3ajdT3Ofcqly4bLOO3gPujSarqor9SxcOsXB7gC+bnKaQlgB6MdABF/X7neSkmPXnTwqqPA8FORY3Ey0WJTl6yjLf3vqK3RtcKOlx4m6zlReeI3DlgHgSGOVIz678DT3KW+7PrXVrsfWavd0m6ZcqpwKMg5dSAts1L1wcO+KNSswZBZS+BqIcC9pUmVzjFeDipbffJyJsAIIruXHibrOVC4AB6D9uhpQHPrY1UXDD9Ym6MP7cCyuQUtdf88/d/VzB5zCjl0LT93dqxxJsanyoheDCkv3237zwYkCCKinIQUAhNC59Q8Onsh1/Ya1Dq5B+2EZugDAp4MRcL6O29OqIYX0QlBhJ54mF6O6kfRV0peDr+8q56qcan+iOG+gLQBH6kiYCQBdcSFpFrqIY9nFblvWLwuFsKL7LpgCgqGw++WV/IUUH6IsX9V54Zun/rDBoSDfJS1fK84W4plJmqr+P9r+RJHUfD2AChyEmQAAaVak8aLKPN4QDp7I4SCsiLJ8G7oY1NKJzx1wCgsp1vK3ltCXKMuXdV/83IiKpU5LWa4l/R5l+eSYBCXK8m2U5TNJI5WjLuq6FMO3AOc8z2sDgCE5U8tHVRxc7A5xusdzziStGN3bWWcqH5gCfbaWv8Vvr6Msn5/SwC9BhQ19qvsXuJP0LsryaZ1EOcrynQUWf6hcdKOO9+JAAzhjF2FLcYEKAK5MQxfwirU4BzzlUoT4XTYLXQDgiq0p5zOkmJ7ayE9Bhd2AzGq2dS1pHGX5+rSSJBuFkah+WMGcecCdubq7FRkAdMFFkcaT0EU85cQHWkNwVaTxLHQRqOWCtUbQR54Xvr9VQ6Hf4xEVC9VLyK9tFMXu5IqMrQyaqH5YAaBhtgYM61IAgHuT0AU8ZueAz4G6v1e5MPsXlSNv36kcxftblOW/SfqP/Z9J+qRyKvFtoFrntv4aumcaugCgSbamnM+QImkqE/ixmKYdUOv8JRoZ2vGUKMs3dlJcq16A8n8l/a8ma2q5XegCGrRVeUHy2Ej+FoDBrxahCzhwL2nzxJ+/9VwHgO67VXkO3divo4OvUOecJFC/TzqY9ufTncrV6ZevbW1nF8Zr++3+133dE/t633B9zzlT+W+VeOqvq9r4uZuIKSDoiYPto324V4MhhfTzrh+zGq9vbGjHc04MK/6HpEXdLVEQjq0Qu3z85zbkNNTTnEGzg13I4b7fVR4HNsdMMbPjxv6L8AIhLXX87ghTublA/1Lx+7cOamiba0mrI3cmm6i83vF583RRpPGoRbtIzOTv738naX7KavF7dtG8lLS093IuP08X3xZpPG3i79AzfO6q7ZaTyM01zLWGcZwfLLsO7mxIIf0cVEwrvvZe0sTHNj4HYcXfNV6+tAPOrtmqgOGwJ1KLAF3fWb/Lqp9hCzPW0o/6p/J/wQOoyo2Knesa/xk9deXtnvmi8iHG7phvthuWhaSFrT0wl7+FJBO1YDczu2n08ZCgsYDiKfZeTu2hx1LuQ+xFkcYrrkEllVNx5h353I3l8Cb+8PrkNfaz6uLndNnEuoJoJ1trZeWpu31IsWm64Uj68aS06od/7jPlt7/8hxov3Q+/A1DfTH5XeL+X9CnK8lGU5Sfva247Ci2iLB+pPI7cNVAjgG65lfSvKMuPvll6LMryhcqbGF9rH4w99fOahYc+vqpclH3puqMoy7dRlicqzwcu10Jr/VazHuw/d7MTP3eJhve5AyqzkGItf9ftMxchhfSwmOak4uvu7KDhlZ28vtZ46Xt7SgWgnqnHvr5LGrk6xthxZKx6xxIA3XSthp742EOaRH5umsYe+niRXT+5XNvhXtKHU25k67LzQSK37+XMRvUN0Y2a+9xt5O9zl3joA2jcwVpCvkKKDy7D5X1QkVR83bzZMo4XZflMTy+y+JpFs5UAw2AjrnxNl/gUZbnzKWU2wmKmcuV4dhYC+s3FzmQ7lQ95XB8/Ro7bP8bMYdv7IcNLh328yMMN8FBHVVxHWd7onHVrayr3n7tzx+0DjbOQYi1/68l9cX3sjmx4SJXU5a4FCwNNVP0gdWk3XACqmXnq54PvkVq2mFciwgqgr24c7ky2lfvjY9A1dWxtCpejKSauhgxXYTfAidyFFVNH7baVs8X27edl7qLtAyEXDgfqWsrfz+61j7WvIlUfVrhqvoxqDhLVquaNFgL0nF2k+jjo/REqAD14mkZYAfTLnapPba3EjltO17wJPG1g5rDtD21azM9xWHExoIdlTlb/P2QPNVhrCjBFGi/lb/vla1cPAB6LVH1Y4ar5MqqzJ6FV55gP6UQBNGHmoY8PobcQJqwAeqnR6R4vmDtuf+y4/ZdMHbX7vQWjc3/heGrBxEGbbdSLz52NOAdar0jjhfxsuSw5HC31lEgV16doU/qt8iBV9WQya74MoLcmjtt3Pr/tWBZWTAOXAaAZ176uV+wY1ruQs0jjidwsyHavFh9rHU4teG+jFPvsxuODB9f9nDtuHziZPYD/6Km7WzkeLfVY9Pq3/KTOIpbO1JwCcklKCrzOLqhczo++9TG/rYqaI7UAtMu9/D+UWHvuz4eJo3a97+5RlU0tcDEFZOKgzTaZ+urIfoa+++oPaBsLKb556s75lK6nVA0qdi6KOIXdWFQNUKbNVwL0zsRx+1PH7dc1F3NfgS5bBrgRXnnuz4eJgzZv2jKK7ggzB20mDtpsi2tbYNantef+gFawh+69Dimk6kHFxkURDZhX/P6xgxqAvkkctv21DSu9P8UOxPPAZQCobxGgz02APp0p0jiRm2kfcwdtOmFTh5oOrX0tdhfCMkCfmwB9AkFZSLH21N0+pNh46u8nVYOKttqELgDoocRRu/dq+cWqj5X8AThxE+CprtoavJ4gcdDmTcvWOTvGvOkGLQTqm7sQ720Hf56Ak9guUGu5CZKfMgt5futLUDGr+P07BzUAvWHrU7g6CK7aPj/ZzEMXAKCyZcC++xRuJg7anDto07WVgzYTB22GtgpdANB3AUKKD6Gn6r2p+P2JiyJOYTdUs4ovWzVeCNAvI4dtLxy23Zgoy5e25ZOvEwKA060D9r2V2wWIfXrroM2/ijR20GznJKELcGAZsO8bufl5BVrjIKS49NTl19AhhVSOqNhW+P5zN2WcZK5qNxL3IqgAXpM4ave2Y0OkV6ELAHC02xDTPg6E7Lsx7Izm3Dh0AQ2779h5HeiihfyFFNdRls889fWiqkGFr3+go9jJ9Kriy0KsBg50zchRu2tH7bqyCl0AgKNtAve/Ddx/U0ahC+i5MxsN3BebwP1vA/cPOFWk8VLV73fr+h5l+dRTX6+KVPEAU6TxxEkl9Sw8vQYYmpGjdleO2nVlHboAAEfbhi6gJ8ahCxiAUegCGrQO3P82cP+Aa75CiltJU099HaVyUCE3+2pXVqTxTNXnpIXY4xnoopGLRru2QreNvroNXQeAo2xCF9ATo9AFDMA4dAEAcOBW5Taku9CFHIrsxr3KStUTW9AjGBsyN6/4stZviQi0iIsF4bp6w78NXQCAo+wC978O3H9TRqELGIDz0AU0aB26AAAnuZc0aVtIIT1sT7qq8JozhR9VsVL1lfgXjKYAgtqFLqCmTegCAAC9Mg5dQI/sQhcAdNi9ypEU29CFPKVOUCFJ81CjKmxBkaqLet6JtSmA0NahC6hpF7oAAEfZhi6gJ8ahCxiA89AF9MgmdAFAhyVt3rUnkn7MG68y/eNC0sxBPS8q0niqeguKzNo4nAVooyKNk9A1tMwmdAEAXtfWJ0IdVHXEKoZtG7oAALV8aHNIIT2MqJCkZcXXfva517aFFN9qvPR7lOWrZqsBAAAAho2AEIArh0HFosbrVz6mgJwQUtyrZdusAAAAYNCq7loHAE37ZvfYrfVm/x9Rlu+KNL5WtakVF5LWRRo7287khJBCkv5L0qxI4+YKQmhJ6AKAARuFLgAAAACN+Fak8daWgWidN49+P1e5o0eV+YmXchRWFGk8l/T5hCb+s6FSAAButq1tg03oAtALm9AFAABQ0cru4zehC3ks+uk35TyzRY12LiVtmlqzokjjUZHGa9UPKf53E3UAA7UNXUDLjEIXALdYbBlN4OcIANBBZyoHHYxDF/JY9MSfLVRtB5C9C0l/F2lce+vSIo3PbRTFRvXn791L+p81XwsMnsOFscaO2nVtFLqAmm5CFwAAAICT3HroYx9WjDz0dbRfggp7IjA9oc3PkrYWWIyOeYGNoJirfJL7WadtjTWR9H9OeD0AN85DF1DTKHQBAF5V5wELnkbI6d596AIAdEYif2GFl40yjvV4jQpJUpTl6yKNv6j+1Isze+3nIo1vJa1VjpLYHnzPWOUNQKJy6kgTPljtSUPtAWhOV1c5H4UuAMCrtqELACrYhC4AQDfYhhdTlffTpzzMP4aztSfreDKokKQoy+d2w3/qzcWlmgsiXvIpyvKlh36AIbiRg2ChSONxGxfreUVXAxYnijQeOZwe9JpzB23yJB742c5Bm78HPG4AQKdFWb6x+/K1BhRWPLVGxaGJ/Aw1OdV1lOWL0EUAeFUSuoAqOj46a+eo3ZGjdo8xdtDm1kGbQJdtHLQ5dtAmAAyGPehL5Gfq2KWkpYd+XvRiUGEpSqJ2hxXXUZZPQxcB9MzaUbuJo3ZdSUIXcIJN6AIAdNLWQZuJgzYBYFAsrJh66u59kcZLT3096bURFW0PKz4RUgBObB21+75Ni/QcYRK6gBYaB+z73EGbOwdtAl22ddDmxEGbADA4UZavJH3w1N1VyLDi1aBCam1Y8YHpHoAzG4dtTx223RjbT9rH+jqu7By1e+6o3WO4eD82DtoEOivK8rWDZi/atu0dAHSVrcvoM6xYeOrrJ0cFFVIZVkRZPpb01V05R7mX9C8WzgTccbzg5cxh202ahS7gRBtH7SaO2n1Rx0biAF3nYovSmYM2AWCQ7F74i6fuPtrOI14dHVT8eEGWzyT9oTB7QN9IGnVw1wCgi1xcqErlk7Wpo7YbYU/+rkLX0VKjQP2OHbW7cdQu0GUbB21OCRwBoDlRls8lXXvq7pvv6/fKQYX0Y27MSP5GV9yrXI8i+DYpwICsHLY9b/kF6zx0AadyNHxbCjeEO3HU7s5Ru0CXrR20eSZGVQBAo2y9xl6GFbWCCunHVJCZpN/l9h/nWuUoioXDPgD8au2w7Qu1NAywLUkZTfGySV/6dBjoAJ1lD6RcmLU8pAaAzrGwwtVI6McWto6bc7WDih8NZPnW/nF+VzlP5u7UNlWOoLiW9HuU5VNGUQD+2RSrJj7Pz/looUBr2AX0MnAZTXJ10po6avdJNoLDxUKaIaYwAl3x3UGbZ+rXMRYA2mIiPxtfnEla+wgrTg4qfjRUBhbzKMtHkv6lMrS40fEXgvcqT4ofVI6gmEZZvm2qPgC1rFy337Kna0uVoz36Yuuo3UvPIdPcUbsbR+0CfbBy1O77Io0njtoGgEHyvEunl7DijYtG7UnsZv97uxEZ22/3/707+J4toQTQSktJHx22vz/QBV9/xvaJfh+yBgc2cjeNZS4PO4DYSdDV32HjqF2g86IsX9qWdGcOml/acX/joO2T2RzskaQl16cAuiLK8p09SNrI/YO3/TX8yNU1vJOg4jErfn3wRysf/QI4TZTlmyKNb+Vm2P3epQKHFRZS9HFdio3Dtt8WaTzzsH7Q0mHbG4dtA32wlJuw+jCk3jhovzYLKb7Zbz8XaXwtaU5gAaALLKyYqLz3dhE0H3L6wLGxqR8AemvhoY99WHHuoa+f9Dik8LFQ5NzlsD97musyJFs7bBvog4XDtr3Ncz6WnQ++PfrjK0n/XaTxsm3rKgHAUywATuRnLS5n1/AEFQBeFGX5Um4X1dy7lLT1dSFYpPF5kcZr9TSkOOBiQbw9Zzca9lTT5bSjO56QAi+zz4jLnd1+PI1z2MerijQeF2m80cvngytJfxVpHLxeAHhNiLCi6UYJKgAcY+6pnzOVF4ILl6Mr7CZ4K+mtqz5aZOW4/cZvNJ55qtm0teP2gb6YO25/f9x33c+TrN+/dfzorbd6CCymruoCgFNZWDHz1N2lXb81hqACwKs8jqrY+yhp0/RFYJHGiY2i+Cb38/baYuWhj0YCJnt/NvIzymXloQ+g8zyMqtj7XKSxz1F10yKNt5I+12ziraRvVvO0scIAoEF2Df/BU3dXTYYVBBUAjjX33N+FHi4CF3WnFxRpPCrSeGYXpH9pGKMofrDFjVxO/zj0UeX0naPfL5uCM7UA6S+5XZNi7y7K8pWHfoC+mMnP8OELORytYMeb/fngm5pZFf/wXDVr2ZbbANDZsMLLrh8Aus+2qpvK/43+hcob4I9FGt+pHLK/1cPQ/W2U5dsijUcqt5OTyi2Q918+bnzbbiF/W6+e6eH9ule5s8b6ie8bKdz7swzQJ9BZtor8XNKfnrp8q3JnoYXK0U8rSes6q8pbaJrYl8vj4IXKf5+51b0Ive02AOzZdfxYbtf/2rsq0nhz6s5wBBUAqpipnMsbyoUepgX8GK5bpHGYajoiyvK1hTyu99R+7Ex2w+G535fcy89ONkCvRFm+sC3vfH6ez1Qe868kyY5jGz1sLby1r72xpHP7GtuX72l+ZyrPTzN7qjgnsADQBlGW70d9+Zhi+2eRxjsbzVELQQWAo0VZvinS+Ivqz+lFOHO5X6CyC3jKCdQ3VRkShFrj58K+fI0QO8WZpITjDYA2ibJ8ag/4fIQV34o0Vt2wgjUqAFQSZflc0k3gMlBRgAVR24jRFMAJbGHNWeAyuuJeZbADAK0SZflU0q2n7r7VXSSZoAJAHVP5WVgNzZqGLiAwhmADJ7LQ08cuIF03s60BAaCNEvkLK1Z1FsUnqABQmT1VmwQuAxVFWb7WcEfD3J66qBOAkj2NG+qx5BhfTpmXDQCu2YObRH7CijNJ66phBUEFgFrsptfXVkdozlTDHA0zDV0A0DMT+Xsa1yXXNkUSAFrtIKzwcV1YOaxgMc32+xK6gA4Yyc+CMHjE81ZHaIBt5TrTsBbW/MQQbKBZtmVponL7YbaBLl3baBMA6IRHx3LXCyWfSVoWaXzUQsMEFS1HKv86+3ARVATieasjNMACpkTDeM+umfIBuEFY8ZMbsdAogA6yXf0S+QkrLlWOrHg1rOjr1I9d6AKAIbEnSCyu1iGeV3wO5VbcOABOeZ7n3FbXUZazFSmAzrKRp4mn7vZhxflL39TLoIIhvuioTq8bYDe+fZqqdCfpU+giHEvU35uLW0ncOAAeHIQV38NWEgTTPQD0gt1D+1p/7lLS8qVv6GVQAXTUJnQBp7KpSn1YYPNW0lg9eE9e0uMnoYQUgGdRlu+iLJ9I+hq6Fo8+EFIA6BPbscjXtfz7Io2Xz9biqYgQ2DYLXbMLXUAT7AD3Tt0dIXKtAd3k9jCsIKQAAoqyfCbpD3X3HHCMO0n/YgtSAH1kxzZfo4qvngsr+hxUbEMXAFS0CV1AU2zr0pG6Fxh+irJ8OrSbXHsSOlb31xm5jrJ8PLT3D2ibKMtXKs8BfZwK8lXSmGnGAPrMFiL3dV14VaTx4pcaPHUewiZ0AUBF69AFNMlufhOViWzbn6zdqnw6tghdSEg2hPmD2v9+PXYvhmADrXIwFeQPlSMQuu5O0rsoy2eEoQCGwPNi+R+LNJ7+1L+njkPYhC4AqGgTugAX7OZ/rPaOrvhiT+E3oQtpAxvuN1Z736/HblQ+3VyGLgTAr2x0xVjlYstdC0GlsuZPUZaPbLQgAAyG57Di22FY0dugwk4mXTwhYphu+/yEJsryrY2ueKf23AB/l/S7LQCKAwfvV5ufhO6fbiZRlm9DFwPgeTa6Yq5yOsgXtfe4cmi/89No6KPtAAzeTP7WMvsRVvQ2qDDr0AUAR1qHLsCHKMvXLQgsvqu8wZ1wg/uyKMtXUZaPVE4HaUvAdCPpD55uAt2zDyxaeFw59F0Px5hFnx8iAMAxAiy8/q1I4/EbT52FspL0PnQRwBGWoQvwyW4wkyKNRypT2omkC4dd3qk8HiwqhhNblU//mrZ20KYzNq1i6fH9eqzu+9dFS3Xs56OGpZr/O24bbq8uF8eLrYM2g3t0XJnY19sApdyr/HlcSVr1OJhYqp/Hlq36/blbO2p366jdKvr2vq0D9u1clOW7Io0TldeBPiS//fPPP5768q9I43NJ/y90HaeIsvy30DW0nX1o/gpdxwnu7OnSoBVpPFZ5oZqomYvVG9nFJ+tPNO/g5iJROf+8yeDiTuWaLWuV79+2wbYBtJRdtyUqjyn7X88a7mZ/fNlIWjMyCwDaqddBhSTZvqxXoeuoi6DidT0IKr6wTsKvLLgYqbxQlcqL1udsJO1UJukbggn/7AZjrPI9G9kfJ0e8dG2/bvXw/u0aKwxAp51wbNmoPC9IdpwhlACA7hhCUJGowzexBBWv6/p7rHJBx23oIgAAAACgDfq+mOY+Pfe18AdQ1TUhBQAAAAA86H1QYRahCwCeMQ9dAAAAAAC0ySCCCltZugt7dmNYGE0BAAAAAI8MIqgw09AFAI/MQxcAAAAAAG0zmKDC1qr4HroOwHxhNAUAAAAA/GowQYWZSboPXQQG706smwIAAAAATxpUUGFPsOeBywCmUZbvQhcBAAAAAG00qKBCkqIsX4gpIAjni01DAgAAAAA8YXBBhZlKug1dBAbnJsryeegiAAAAAKDNBhlU2LD7qVivAv7cSpqELgIAAAAA2m6QQYUkRVm+kZSIsALu3Yt1KQAAAADgKIMNKqQfYcUscBnot3tJif2sAQAAAABeMeigQpKiLF9K+iBGVqB5hBQAAAAAUNHggwrpR1iRiLACzbkVIQUAAAAAVEZQYQ7WrGA3EJzqRoQUAAAAAFALQcWBg7Die9hK0GFfoyxPWDgTAAAAAOohqHgkyvJdlOUTsW4FqrmT9C7K8lnoQgAAAACgywgqnmHrVozE6Aq87qukcZTl69CFAAAAAEDXvQldQJvZ8P1JkcaJpLmktyHrQevcSJpGWb4NXQgAAAAA9AUjKo4QZfk6yvJE0juVN6cYtu8qp3kkhBQAAAAA0CxGVFRgQ/uTIo3HkmaSJpLOwlUEj+4lLSUtCCcAAAAAwB2Cihpsd5CpJBVpPFEZWCSSLgKVBDfuJK0kraMsX4UtBQAAAACGgaDiRHYDu5IkG2lx+DUS4UVX3EnaSlpL2kjaMHICAAAAAPwjqGiQjbTYPPX/LMQ491fNoGxUrh9S1c7eMwAAAABAS/x/ByfmJv7i4p4AAAAASUVORK5CYII=\" alt=\"authentik\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__mcp\" src=\"data:image/png;base64,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\" alt=\"Model Context Protocol\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">🏆</span><span class=\"skillTracker__skill\" data-state=\"current\">A scope per tool, and a refusal clients can act on</span></span></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/\">Part 6</a> put <code>required_scopes=[\"mcp:invoke\"]</code> on the JWT\nverifier and called the server authorised. It is, in the sense that an unauthenticated caller\ngets nothing. It is not, in the sense that matters: <code>mcp:invoke</code> opens every tool on the server.\nThe same token that lets an agent run <code>find_customer</code> lets it run <code>issue_refund</code>.</p>\n<p>That is the whole gap. A read-only agent and a refund-issuing agent hold identical credentials,\nand the only thing standing between \"look up Maria\" and \"refund invoice 2\" is that nobody asked.</p>\n<p>This post closes it, badly first and then properly, because the badly is what most people ship\nand the difference only shows up in what the client can <em>do</em> about a refusal.</p>\n<p>Both versions refuse the same call — a read-only token asking for <code>issue_refund</code>. They differ in\n<em>where</em> the scope is checked, and that one choice decides what the client gets back:</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<p>Parts 5 and 6, specifically:</p>\n<ul>\n<li class=\"\">Authentik issuing client-credentials tokens to an <code>agent-tools</code> provider\n(<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/\">part 5</a>)</li>\n<li class=\"\">A FastMCP server verifying those tokens against the JWKS endpoint\n(<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/\">part 6</a>)</li>\n<li class=\"\"><code>~/mcp-auth/token.sh</code>, which takes a scope string and returns an access token</li>\n</ul>\n<p>Part 6's server stays on <code>:8770</code> throughout. The two versions below run on <code>:8771</code> and <code>:8772</code>\nso you can compare all three without stopping anything.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--two-new-scopes\">Step 1 — Two new scopes<a href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/#step-1--two-new-scopes\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Two new scopes\" title=\"Direct link to Step 1 — Two new scopes\" translate=\"no\">​</a></h2>\n<p>Scopes are Property Mappings in Authentik. <strong>Customization → Property Mappings → New Property\nMapping → Scope Mapping</strong>, twice:</p>\n<table><thead><tr><th>Name</th><th>Scope name</th><th>Description</th></tr></thead><tbody><tr><td><code>office-read</code></td><td><code>office:read</code></td><td>Look up customers and invoices</td></tr><tr><td><code>office-refund</code></td><td><code>office:refund</code></td><td>Issue refunds</td></tr></tbody></table>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Two new scope mappings alongside the existing gateway-invoke and mcp-invoke\" src=\"https://development-wec.wiline.com/docs/assets/images/h10-scope-mappings-c80d52648d4347ac31467ecd7bfbc924.png\" width=\"1901\" height=\"831\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Creating them is not enough. A provider will only issue a scope it has been given, so open\n<strong>Applications → Providers → agent-tools → Edit</strong> and move both into <em>Selected Scopes</em>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The agent-tools provider with mcp-invoke, office-read and office-refund selected\" src=\"https://development-wec.wiline.com/docs/assets/images/h10-provider-scopes-845343ec44444c00ad1c5931086ff83c.png\" width=\"1902\" height=\"944\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Note the line under the picker: <em>\"Select which scopes can be used by the client. The client still\nhas to specify the scope to access the data.\"</em> Both halves matter. Selecting a scope here does not\nput it in every token — it permits the client to ask. A client that asks for nothing gets nothing,\nwhich is why every <code>token.sh</code> call below passes an explicit scope string.</p>\n<p>Confirm the token actually carries what you asked for:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/mcp-auth/token.sh </span><span class=\"token string\" style=\"color:#e3116c\">\"mcp:invoke office:read\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">cut</span><span class=\"token plain\"> -d. </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-f2</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> base64 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq .scope</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">\"mcp:invoke office:read\"</span><br></div></code></pre></div></div>\n<p>If that comes back without <code>office:read</code>, the scope is not on the provider — fix that before\nwriting any server code, or you will spend an hour debugging enforcement that is working\ncorrectly on a token that was never scoped.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--the-obvious-implementation\">Step 2 — The obvious implementation<a href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/#step-2--the-obvious-implementation\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — The obvious implementation\" title=\"Direct link to Step 2 — The obvious implementation\" translate=\"no\">​</a></h2>\n<p>Keep <code>required_scopes=[\"mcp:invoke\"]</code> on the verifier as the price of admission, then ask per\ntool whether the caller holds what <em>that</em> operation needs. FastMCP exposes the verified token\nthrough <code>get_access_token()</code>, so a decorator can read the claims the verifier already checked:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastmcp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">exceptions </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> ToolError</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastmcp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">server</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">dependencies </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> get_access_token</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">requires</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">scope</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">decorate</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">fn</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@functools</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">wraps</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">fn</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">wrapper</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token operator\" style=\"color:#393A34\">*</span><span class=\"token plain\">args</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">**</span><span class=\"token plain\">kwargs</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            token </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> get_access_token</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            held </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">set</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token builtin\">getattr</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">token</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"scopes\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> scope </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> held</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token keyword\" style=\"color:#00009f\">raise</span><span class=\"token plain\"> ToolError</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                    </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f'insufficient_scope: this tool requires \"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">scope</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"; '</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                    </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f'token carries </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation builtin\">sorted</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">held</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation\"> </span><span class=\"token string-interpolation interpolation keyword\" style=\"color:#00009f\">or</span><span class=\"token string-interpolation interpolation\"> </span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">\"nothing\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> fn</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token operator\" style=\"color:#393A34\">*</span><span class=\"token plain\">args</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">**</span><span class=\"token plain\">kwargs</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> wrapper</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> decorate</span><br></div></code></pre></div></div>\n<p>Then one line per tool:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@mcp</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">tool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@requires</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"office:read\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">find_customer</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">query</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@mcp</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">tool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@requires</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"office:refund\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">issue_refund</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">invoice_id</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">int</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><br></div></code></pre></div></div>\n<p>Run it on <code>:8771</code> and drive both tools with both tokens:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Two tokens, two tools: each token allows one and refuses the other\" src=\"https://development-wec.wiline.com/docs/assets/images/h10-per-tool-enforcement-36365bd959bc324dd447cfdbe681525c.png\" width=\"845\" height=\"564\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">--- token: mcp:invoke + office:read ---</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  find_customer  : ALLOWED -&gt; [{'id': 1, 'name': 'Maria Alvarez', ...}]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  issue_refund   : REFUSED -&gt; insufficient_scope: this tool requires \"office:refund\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">--- token: mcp:invoke + office:refund ---</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  find_customer  : REFUSED -&gt; insufficient_scope: this tool requires \"office:read\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  issue_refund   : ALLOWED -&gt; Refunded 80.00 on invoice 2.</span><br></div></code></pre></div></div>\n<p>That is real enforcement. The refund token cannot read, the read token cannot refund, and the\nrefusal names the missing scope. For a lot of deployments this is where you stop.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--why-that-refusal-is-not-good-enough\">Step 3 — Why that refusal is not good enough<a href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/#step-3--why-that-refusal-is-not-good-enough\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — Why that refusal is not good enough\" title=\"Direct link to Step 3 — Why that refusal is not good enough\" translate=\"no\">​</a></h2>\n<p>Watch the HTTP layer rather than the client library.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">TOK</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/mcp-auth/token.sh </span><span class=\"token variable string\" style=\"color:#e3116c\">\"mcp:invoke office:read\"</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST http://127.0.0.1:8771/mcp </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$TOK</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Accept: application/json, text/event-stream\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"issue_refund\",\"arguments\":{\"invoice_id\":2}}}'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The tool-body check returns HTTP 200 with the refusal inside the body\" src=\"https://development-wec.wiline.com/docs/assets/images/h10-200-no-challenge-ec64c16f2444a9ed7bcacaed0d917f3e.png\" width=\"827\" height=\"346\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">HTTP/1.1 200 OK</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">content-type: text/event-stream</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">data: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"content\":[{\"text\":\"insufficient_scope: this tool</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">requires \\\"office:refund\\\"; token carries ['mcp:invoke', 'office:read']\",\"type\":\"text\"},</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">\"isError\":true}}</span><br></div></code></pre></div></div>\n<p><strong>HTTP 200.</strong> The request succeeded; the <em>tool</em> declined. That is correct JSON-RPC semantics and\nit is useless to an OAuth client.</p>\n<p>An OAuth client that wants to step up — go back to the authorization server and ask for\n<code>office:refund</code> — is watching for a <code>401</code> or <code>403</code> with a <code>WWW-Authenticate</code> header telling it\nwhat to request. It does not parse English out of a tool result. So the refusal is legible to a\nhuman reading logs and invisible to the machinery designed to handle exactly this case.</p>\n<p>It also arrives late. The tool body runs after routing, after session setup, after the server has\ncommitted to a successful response. By then there is no status line left to change.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--the-challenge-the-spec-actually-wants\">Step 4 — The challenge the spec actually wants<a href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/#step-4--the-challenge-the-spec-actually-wants\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — The challenge the spec actually wants\" title=\"Direct link to Step 4 — The challenge the spec actually wants\" translate=\"no\">​</a></h2>\n<p>Getting a <code>403</code> means checking scopes <em>before</em> the JSON-RPC layer answers, which means ASGI\nmiddleware wrapping the FastMCP app:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">ScopeChallengeMiddleware</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">__init__</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">self</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> app</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        self</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">app </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> app</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">__call__</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">self</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> scope</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> receive</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> send</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> scope</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">!=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> scope</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"method\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">!=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"POST\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> self</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">app</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">scope</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> receive</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> send</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Buffer the body so we can inspect it and still pass it on.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        chunks</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> more </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">while</span><span class=\"token plain\"> more</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            msg </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> receive</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            chunks</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">append</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">msg</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"body\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">b\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            more </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> msg</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"more_body\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">False</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        body </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">b\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">join</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">chunks</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        needed </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">try</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            rpc </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">loads</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">body</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> rpc</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"method\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"tools/call\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                needed </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> TOOL_SCOPES</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">rpc</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"params\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"name\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">except</span><span class=\"token plain\"> Exception</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">pass</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> needed </span><span class=\"token keyword\" style=\"color:#00009f\">and</span><span class=\"token plain\"> needed </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> token_scopes</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">scope</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"headers\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            challenge </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f'Bearer error=\"insufficient_scope\", scope=\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">needed</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\", '</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f'resource_metadata=\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">RESOURCE_METADATA</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\", '</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f'error_description=\"This operation requires the </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">needed</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> scope\"'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> send</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http.response.start\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"status\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">403</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                        </span><span class=\"token string\" style=\"color:#e3116c\">\"headers\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">b\"content-type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">b\"application/json\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                    </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">b\"www-authenticate\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> challenge</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">encode</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> send</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http.response.body\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                        </span><span class=\"token string\" style=\"color:#e3116c\">\"body\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">dumps</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"error\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"insufficient_scope\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                            </span><span class=\"token string\" style=\"color:#e3116c\">\"scope\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> needed</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">encode</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Replay the buffered body downstream.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        replayed </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">False</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">replay</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">nonlocal</span><span class=\"token plain\"> replayed</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> replayed</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                replayed </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http.request\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"body\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> body</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"more_body\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">False</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> receive</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> self</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">app</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">scope</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> replay</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> send</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">app </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ScopeChallengeMiddleware</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">mcp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">http_app</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">stateless_http</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Same request, against <code>:8772</code>:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The middleware returns 403 with a WWW-Authenticate challenge naming the missing scope\" src=\"https://development-wec.wiline.com/docs/assets/images/h10-403-challenge-2a8d3ff52be96195ff042471605c0c4e.png\" width=\"827\" height=\"247\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">HTTP/1.1 403 Forbidden</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">www-authenticate: Bearer error=\"insufficient_scope\", scope=\"office:refund\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  resource_metadata=\"http://127.0.0.1:8772/.well-known/oauth-protected-resource\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  error_description=\"This operation requires the office:refund scope\"</span><br></div></code></pre></div></div>\n<p>Now a client has something to act on: the status says refused, <code>scope=</code> says what to ask for, and\n<code>resource_metadata</code> says where to look up how. That is step-up authorization as a protocol rather\nthan as a log message.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--what-it-cost\">Step 5 — What it cost<a href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/#step-5--what-it-cost\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — What it cost\" title=\"Direct link to Step 5 — What it cost\" translate=\"no\">​</a></h2>\n<p>The middleware works. It is also worse code than the decorator, in three specific ways, and\npretending otherwise would be dishonest.</p>\n<p><strong>It does not know what a tool is.</strong> ASGI middleware sees bytes and headers. To find out which\ntool is being called it parses the JSON-RPC envelope itself and looks the name up in a table it\nhas to maintain:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">TOOL_SCOPES </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"find_customer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"office:read\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"open_invoices\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"office:read\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"issue_refund\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">  </span><span class=\"token string\" style=\"color:#e3116c\">\"office:refund\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>That table is a second source of truth. Add a tool and forget the entry and it is unprotected —\nsilently, because the middleware just passes through anything it does not recognise. The\ndecorator could not have that bug: the requirement sat on the function.</p>\n<p><strong>It decodes the token unverified.</strong> The middleware runs <em>upstream</em> of the verifier, so the\nverified claims do not exist yet. It splits the JWT and base64-decodes the payload without\nchecking the signature:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">payload </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> auth</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">split</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">split</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\".\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">claims </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">loads</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">base64</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">urlsafe_b64decode</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">payload </span><span class=\"token operator\" style=\"color:#393A34\">+</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"=\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">*</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token builtin\">len</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">payload</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">%</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">4</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>This is not the hole it looks like — the verifier still runs downstream and still rejects a\nforged token, so nothing reaches a tool on a bad signature. But the <em>scope decision</em> is made on\nunauthenticated bytes, and the only reason that is survivable is the second check behind it. It\nis a wart, not a vulnerability, and it is the kind of thing worth writing down before someone\nlater removes the \"redundant\" verifier.</p>\n<p><strong>It buffers every request body.</strong> To read the envelope and still pass it downstream, the\nmiddleware drains <code>receive()</code> into memory and replays it. Fine for JSON-RPC calls; think harder\nbefore putting this in front of large uploads.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting--the-errors-this-run-actually-produced\">Troubleshooting — the errors this run actually produced<a href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/#troubleshooting--the-errors-this-run-actually-produced\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting — the errors this run actually produced\" title=\"Direct link to Troubleshooting — the errors this run actually produced\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"insufficient_scope-on-a-tool-you-did-grant\"><code>insufficient_scope</code> on a tool you did grant<a href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/#insufficient_scope-on-a-tool-you-did-grant\" class=\"hash-link\" aria-label=\"Direct link to insufficient_scope-on-a-tool-you-did-grant\" title=\"Direct link to insufficient_scope-on-a-tool-you-did-grant\" translate=\"no\">​</a></h3>\n<p>Check the token, not the server:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/mcp-auth/token.sh </span><span class=\"token string\" style=\"color:#e3116c\">\"mcp:invoke office:refund\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">cut</span><span class=\"token plain\"> -d. </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-f2</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> base64 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq .scope</span><br></div></code></pre></div></div>\n<p>If <code>office:refund</code> is missing, the scope exists as a Property Mapping but was never moved into\nthe provider's <em>Selected Scopes</em>. Authentik silently drops scopes a client is not permitted to\nrequest rather than erroring, so the token comes back valid and short.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-middleware-never-fires\">The middleware never fires<a href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/#the-middleware-never-fires\" class=\"hash-link\" aria-label=\"Direct link to The middleware never fires\" title=\"Direct link to The middleware never fires\" translate=\"no\">​</a></h3>\n<p>It only inspects <code>POST</code>. MCP clients open a <code>GET</code> for the event stream first, and that request\ncarries no JSON-RPC envelope — if you are watching the wrong request you will conclude the\nmiddleware is dead. Confirm with the <code>curl</code> above, which is a single <code>POST</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"every-tool-suddenly-returns-403\">Every tool suddenly returns 403<a href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/#every-tool-suddenly-returns-403\" class=\"hash-link\" aria-label=\"Direct link to Every tool suddenly returns 403\" title=\"Direct link to Every tool suddenly returns 403\" translate=\"no\">​</a></h3>\n<p><code>TOOL_SCOPES</code> is keyed by the tool's registered name, which is the <em>function</em> name, not the\ndecorated label. Rename a function and the table stops matching. The pass-through case is the\ndangerous direction (unprotected), but a typo in the table produces this one.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-403-body-is-empty-in-some-clients\">The 403 body is empty in some clients<a href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/#the-403-body-is-empty-in-some-clients\" class=\"hash-link\" aria-label=\"Direct link to The 403 body is empty in some clients\" title=\"Direct link to The 403 body is empty in some clients\" translate=\"no\">​</a></h3>\n<p>The challenge lives in the <code>WWW-Authenticate</code> <strong>header</strong>. Clients that only log response bodies\nwill show you <code>{\"error\":\"insufficient_scope\"}</code> and nothing about which scope. Use <code>curl -i</code>.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-this-did-and-didnt-buy-you\">What this did and didn't buy you<a href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/#what-this-did-and-didnt-buy-you\" class=\"hash-link\" aria-label=\"Direct link to What this did and didn't buy you\" title=\"Direct link to What this did and didn't buy you\" translate=\"no\">​</a></h2>\n<p><strong>It bought</strong> genuine per-tool authorisation: two tokens that differ by one scope, each able to\nrun exactly one of two tools, proven at the HTTP layer rather than asserted. And with the\nmiddleware, a refusal a client can programmatically recover from.</p>\n<p><strong>It did not buy</strong> a clean design. Both implementations are compromises pointing opposite ways:</p>\n<table><thead><tr><th></th><th>tool decorator (<code>:8771</code>)</th><th>ASGI middleware (<code>:8772</code>)</th></tr></thead><tbody><tr><td>Scope requirement lives</td><td>on the function</td><td>in a separate table</td></tr><tr><td>Reads verified claims</td><td>yes</td><td>no — decodes unverified, verifier runs after</td></tr><tr><td>Refusal</td><td>HTTP 200, <code>isError: true</code></td><td>HTTP 403 + <code>WWW-Authenticate</code></td></tr><tr><td>Client can step up</td><td>no</td><td>yes</td></tr><tr><td>New tool unprotected by default</td><td>no</td><td><strong>yes</strong></td></tr></tbody></table>\n<p>The honest summary is that the correct protocol behaviour requires leaving the abstraction the\nframework gives you, and the ergonomic version cannot produce it. If your clients do not\nimplement step-up — and most agent clients today do not — the decorator is the better trade.\nIf they do, you pay for it with a table you must remember to update.</p>\n<p><strong>It also did not buy</strong> authorisation that survives the tool doing something else. <code>issue_refund</code>\nis gated on <code>office:refund</code>; nothing stops a future <code>find_customer</code> from being edited to write.\nScopes gate entry, not behaviour — which is the same boundary\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/\">part 4</a> drew around policy-in-the-tool.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>The obvious remaining gap is that both versions trust the token's scope list and nothing else.\nNeither asks <em>who</em> the caller is or <em>what</em> they are acting on — a token with <code>office:refund</code>\nrefunds any invoice, for any customer, for any amount. That is object-level authorisation, and\nit does not live in OAuth scopes at all.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>A scope per tool, and a refusal clients can act on</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://modelcontextprotocol.io/specification/basic/authorization\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">MCP specification — authorization</a></li>\n<li class=\"\"><a href=\"https://datatracker.ietf.org/doc/html/rfc6750#section-3\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">RFC 6750 §3 — the <code>WWW-Authenticate</code> challenge</a></li>\n<li class=\"\"><a href=\"https://datatracker.ietf.org/doc/html/rfc9728\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">RFC 9728 — OAuth 2.0 Protected Resource Metadata</a></li>\n<li class=\"\"><a href=\"https://docs.goauthentik.io/docs/providers/property-mappings/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Authentik — property mappings and scopes</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/",
            "title": "Give each MCP tool its own scope, and return a refusal the client can act on",
            "summary": "Part 6 gated the whole server with one scope, which meant the token that let an agent look up a customer also let it issue refunds. Splitting that per tool is easy. Making the refusal useful is not: the obvious implementation returns HTTP 200 with the error buried in the body, so a client has nothing to trigger step-up authorization on. Getting the 403 and WWW-Authenticate challenge the spec defines means leaving the framework's abstraction entirely — and the version that does it decodes the token a second time, unverified.",
            "date_modified": "2026-09-18T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "security",
                "mcp",
                "oauth2",
                "authentik",
                "jwt",
                "scopes",
                "agents",
                "self-hosting",
                "hardening"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/",
            "content_html": "<div class=\"tutorialHero\"><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">9</span><span class=\"skillTracker__skill\" data-state=\"current\">Run the model's own code without trusting it</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Every agent tutorial in this series so far has given a model <em>tools</em> — functions you wrote,\nwith arguments you defined. This post is about the other thing agents do, which is write code\nand then run it.</p>\n<p>That is a different risk, and the difference is worth being precise about. A tool call is the\nmodel choosing from a menu you control. Executing generated code is the model handing you\nsomething nobody has ever read, which you then run on your machine.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>One WEC Instance: 8 vCPU AMD EPYC 7601, 15 GB RAM, Ubuntu 22.04, kernel 5.15, systemd 249.\nDocker rootless <strong>29.8.1</strong> from <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/\">Part 8</a>, running\nalongside the system daemon. Test image is <code>python:3.12-alpine</code>. The host had 20 containers\nrunning throughout.</p><p>Every number and error message below is from that run.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"why-this-needs-its-own-post\">Why this needs its own post<a href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/#why-this-needs-its-own-post\" class=\"hash-link\" aria-label=\"Direct link to Why this needs its own post\" title=\"Direct link to Why this needs its own post\" translate=\"no\">​</a></h2>\n<p>The industry has a name for this. OWASP's\n<a href=\"https://genai.owasp.org/llmrisk/llm052025-improper-output-handling/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">LLM05:2025 Improper Output Handling</a>\nlists it as a vulnerability example, in plain words:</p>\n<blockquote>\n<p>LLM output is entered directly into a system shell or similar function such as exec or eval,\nresulting in remote code execution.</p>\n</blockquote>\n<p>And its advice on how to think about the model is exactly right:</p>\n<blockquote>\n<p>Treat the model as any other user, adopting a zero-trust approach</p>\n</blockquote>\n<p>Here is what makes that page worth reading closely, though. It tells you to treat model output\nas untrusted, it names code execution as the consequence, and then its prevention section talks\nentirely about <em>validating text</em> — output encoding, parameterised queries, content security\npolicies, monitoring.</p>\n<p>Search that page for <strong>sandbox</strong>, <strong>container</strong>, <strong>least privilege</strong> or <strong>isolation</strong> and you\nget zero hits. Not few. None.</p>\n<p>That is not a criticism of OWASP; the risk is about output handling, and validation is the right\nanswer for output that becomes HTML or SQL. But when the output is a program you are about to\nrun, validation stops being possible. You cannot regex your way to knowing whether a hundred\nlines of Python are safe.</p>\n<p>So the guidance names the problem and stops one layer above the fix. This post is that layer.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-the-obvious-version-actually-allows\">What the obvious version actually allows<a href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/#what-the-obvious-version-actually-allows\" class=\"hash-link\" aria-label=\"Direct link to What the obvious version actually allows\" title=\"Direct link to What the obvious version actually allows\" translate=\"no\">​</a></h2>\n<p>The naive implementation is what most people ship first, and it looks responsible — the code\nruns in a container, after all:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> python:3.12-alpine python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"&lt;the model's code&gt;\"</span><br></div></code></pre></div></div>\n<p>Let's ask that container what it can reach.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> python:3.12-alpine python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">import os, urllib.request</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('uid inside     :', os.getuid())</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('can see host / :', sorted(os.listdir('/'))[:10])</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">try:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    r = urllib.request.urlopen('https://api.github.com', timeout=5)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print('internet       : REACHED', r.status)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">except Exception as e:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print('internet       : blocked', type(e).__name__)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> python:3.12-alpine </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">echo \"memory limit : $(cat /sys/fs/cgroup/memory.max)\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">echo \"pids limit   : $(cat /sys/fs/cgroup/pids.max)\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">echo \"cpus visible : $(nproc)\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">echo \"writable fs  : $(touch /proc-test 2&gt;/dev/null &amp;&amp; echo yes || echo no)\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"A container running model code reports uid 0, full internet access returning HTTP 200, unlimited memory, a pids limit of 19136, eight visible CPUs and a writable filesystem\" src=\"https://development-wec.wiline.com/docs/assets/images/h9-naive-sandbox-06b5b3907235d86115ef158babda8df0.png\" width=\"781\" height=\"561\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">internet       : REACHED 200</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">memory limit : max</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">pids limit   : 19136</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">cpus visible : 8</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">writable fs  : yes</span><br></div></code></pre></div></div>\n<p>Read that as a list of what generated code is allowed to do on your machine:</p>\n<ul>\n<li class=\"\"><strong>Reach the internet.</strong> Anything it finds, it can send somewhere.</li>\n<li class=\"\"><strong>Take all the memory.</strong> <code>max</code> means no limit. On this host that is 15 GB, and the kernel\nwill start killing other things to provide it.</li>\n<li class=\"\"><strong>Start 19,136 processes.</strong> Enough to make the host unusable.</li>\n<li class=\"\"><strong>Use all eight cores.</strong></li>\n<li class=\"\"><strong>Write anywhere in the container</strong>, including filling the disk.</li>\n</ul>\n<p>Part 8 moved the daemon off root, so this code can no longer take over the <em>host</em>. That was\nworth doing and it does not help here at all. None of the five items above involves being root.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"closing-it-down\">Closing it down<a href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/#closing-it-down\" class=\"hash-link\" aria-label=\"Direct link to Closing it down\" title=\"Direct link to Closing it down\" translate=\"no\">​</a></h2>\n<p>Docker has flags for every one of these. They are unglamorous and they are the entire fix:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--network</span><span class=\"token plain\"> none </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--memory</span><span class=\"token plain\"> 256m --pids-limit </span><span class=\"token number\" style=\"color:#36acaa\">64</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--cpus</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0.5</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --read-only </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--tmpfs</span><span class=\"token plain\"> /tmp:size</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">16m </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --cap-drop</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">ALL --security-opt no-new-privileges </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  python:3.12-alpine </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">command</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><br></div></code></pre></div></div>\n<p>In plain terms:</p>\n<table><thead><tr><th>Flag</th><th>What it stops</th></tr></thead><tbody><tr><td><code>--network none</code></td><td>No network at all — nothing can be sent anywhere</td></tr><tr><td><code>--memory 256m</code></td><td>Uses more than 256 MB and the kernel kills it, not your other services</td></tr><tr><td><code>--pids-limit 64</code></td><td>A fork bomb hits 64 and stops</td></tr><tr><td><code>--cpus 0.5</code></td><td>An infinite loop gets half a core</td></tr><tr><td><code>--read-only</code></td><td>Nothing can be written to the container's filesystem</td></tr><tr><td><code>--tmpfs /tmp:size=16m</code></td><td>Except a small scratch area, capped, in memory</td></tr><tr><td><code>--cap-drop=ALL</code></td><td>No kernel privileges — no mounting, no raw sockets, nothing</td></tr><tr><td><code>--security-opt no-new-privileges</code></td><td>Nothing inside can gain more privileges than it started with</td></tr></tbody></table>\n<p>Run it, and it fails before the container even starts:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Docker refuses the run with the error NanoCPUs can not be set, as your kernel does not support CPU CFS scheduler or the cgroup is not mounted\" src=\"https://development-wec.wiline.com/docs/assets/images/h9-cpus-refused-0a8c67fa115c08d21d53054814b86f30.png\" width=\"994\" height=\"344\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">docker: Error response from daemon: NanoCPUs can not be set, as your kernel does not</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">support CPU CFS scheduler or the cgroup is not mounted</span><br></div></code></pre></div></div>\n<p>Hold onto that message, because it is misleading. The kernel supports CPU limits perfectly\nwell — this host runs 20 other containers with them available. We will come back to what is\nactually wrong.</p>\n<p>Drop <code>--cpus</code> and everything else applies:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The hardened container reports a 256 MiB memory limit, a pids limit of 64, a read-only filesystem, a writable tmp, and network access blocked with URLError\" src=\"https://development-wec.wiline.com/docs/assets/images/h9-hardened-holds-e5ed1f26eadb61b1c097d1438eae1520.png\" width=\"751\" height=\"396\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">memory limit : 268435456        &lt;- exactly 256 MiB</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">pids limit   : 64</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">cpus visible : 8</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">writable fs  : no</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">tmp writable : yes</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">internet     : blocked URLError</span><br></div></code></pre></div></div>\n<p>Five of six controls working. And <code>cpus visible: 8</code>, because we were not allowed to set one.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"proving-the-gap-is-real\">Proving the gap is real<a href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/#proving-the-gap-is-real\" class=\"hash-link\" aria-label=\"Direct link to Proving the gap is real\" title=\"Direct link to Proving the gap is real\" translate=\"no\">​</a></h2>\n<p>A missing CPU limit sounds mild next to \"unlimited memory\". It is not, because an infinite loop\nis the single most likely thing generated code does by accident.</p>\n<p>This runs eight busy loops inside the otherwise fully locked-down sandbox, for eight seconds:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--name</span><span class=\"token plain\"> burn </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--network</span><span class=\"token plain\"> none </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--memory</span><span class=\"token plain\"> 256m --pids-limit </span><span class=\"token number\" style=\"color:#36acaa\">64</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --read-only </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--tmpfs</span><span class=\"token plain\"> /tmp:size</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">16m </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --cap-drop</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">ALL --security-opt no-new-privileges </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  python:3.12-alpine </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'for i in $(seq 8); do (while :; do :; done) &amp; done; sleep 8'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless stats --no-stream burn</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Docker stats showing the burn container consuming 605.19 percent CPU while memory sits at 1.23 MiB of 256 MiB, network IO at zero and 9 processes\" src=\"https://development-wec.wiline.com/docs/assets/images/h9-burn-605-a829703726df3283acc0afa12d391696.png\" width=\"678\" height=\"208\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">NAME   CPU %     MEM USAGE / LIMIT   NET I/O   PIDS</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">burn   605.19%   1.23MiB / 256MiB    0B / 0B   9</span><br></div></code></pre></div></div>\n<p><strong>605% of a CPU.</strong> Six of the host's eight cores, taken by code inside a sandbox where memory is\ncapped at 256 MB (it used 1.23), processes are capped at 64 (it used 9), and the network is\ncompletely blocked.</p>\n<p>Every control we set is working. The one we could not set is the one being abused.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-is-actually-wrong\">What is actually wrong<a href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/#what-is-actually-wrong\" class=\"hash-link\" aria-label=\"Direct link to What is actually wrong\" title=\"Direct link to What is actually wrong\" translate=\"no\">​</a></h2>\n<p>The error message blamed the kernel. The kernel is fine. The real answer is one file.</p>\n<p>When Docker runs rootless, it does not get to manage resources directly — it can only use what\nthe system has handed to your user account. On modern Linux that handover is called <strong>cgroup\ndelegation</strong>, and systemd decides what gets delegated.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> /sys/fs/cgroup/user.slice/user-1000.slice/cgroup.controllers</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">memory pids</span><br></div></code></pre></div></div>\n<p>Two things. That is the whole explanation:</p>\n<!-- -->\n<p>The daemon knew all along. It said so at startup, ten times, in lines nobody reads:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">journalctl </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--user</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-u</span><span class=\"token plain\"> docker.service </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"no .* support\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The rootless daemon startup log listing ten warnings including no cpu cfs quota support, no cpuset support, no io.weight support and no io.max support\" src=\"https://development-wec.wiline.com/docs/assets/images/h9-delegation-root-cause-801bd67d214cce22c58da71540cddebb.png\" width=\"1233\" height=\"260\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: No cpu cfs quota support</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: No cpu cfs period support</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: No cpu shares support</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: No cpuset support</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: No io.weight support</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: No io.max (rbps) support</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">...</span><br></div></code></pre></div></div>\n<p>This also explains something <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/\">Part 8</a> reported as a\nplain limitation of rootless mode: disk throughput limits not working. Same cause. Not a\nproperty of rootless — a property of what systemd delegated.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"where-this-is-documented\">Where this is documented<a href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/#where-this-is-documented\" class=\"hash-link\" aria-label=\"Direct link to Where this is documented\" title=\"Direct link to Where this is documented\" translate=\"no\">​</a></h3>\n<p>Not in Docker's rootless documentation. I searched the page: <strong>zero</strong> occurrences of\n\"delegate\". The guidance lives upstream, on the\n<a href=\"https://rootlesscontaine.rs/getting-started/common/cgroup2/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">rootless containers site</a>, which\nstates it directly:</p>\n<blockquote>\n<p>By default, a non-root user can only get memory controller and pids controller to be delegated.</p>\n</blockquote>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-fix\">The fix<a href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/#the-fix\" class=\"hash-link\" aria-label=\"Direct link to The fix\" title=\"Direct link to The fix\" translate=\"no\">​</a></h2>\n<p>One drop-in file telling systemd to hand over more:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> /etc/systemd/system/user@.service.d</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token string bash punctuation\" style=\"color:#393A34\"> </span><span class=\"token string bash punctuation operator\" style=\"color:#393A34\">|</span><span class=\"token string bash punctuation\" style=\"color:#393A34\"> </span><span class=\"token string bash punctuation function\" style=\"color:#d73a49\">sudo</span><span class=\"token string bash punctuation\" style=\"color:#393A34\"> </span><span class=\"token string bash punctuation function\" style=\"color:#d73a49\">tee</span><span class=\"token string bash punctuation\" style=\"color:#393A34\"> /etc/systemd/system/user@.service.d/delegate.conf</span><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">[Service]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">Delegate=cpu cpuset io memory pids</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> systemctl daemon-reload</span><br></div></code></pre></div></div>\n<p>The same page warns:</p>\n<blockquote>\n<p>After changing the systemd configuration, you need to re-login or reboot the host. Rebooting\nthe host is recommended.</p>\n</blockquote>\n<p>On systemd 249 that turned out not to be needed. After <code>daemon-reload</code> alone:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> /sys/fs/cgroup/user.slice/user-1000.slice/cgroup.controllers</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">cpuset cpu io memory pids</span><br></div></code></pre></div></div>\n<p>The daemon still needs restarting, because it reads its capabilities once at startup. Restart\n<strong>only the rootless daemon</strong> — not your whole user session, which on this host would also have\ntaken down an unrelated service:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">systemctl </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--user</span><span class=\"token plain\"> restart docker.service</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">journalctl </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--user</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-u</span><span class=\"token plain\"> docker.service </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--since</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"-1min\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-ci</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"no cpu\\|no io\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The delegate.conf drop-in being written, followed by a restart of the rootless docker service and a warning count of zero\" src=\"https://development-wec.wiline.com/docs/assets/images/h9-delegate-fix-1f7414c6361fa1fc13be57138940d1f5.png\" width=\"646\" height=\"206\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">0</span><br></div></code></pre></div></div>\n<p>Zero warnings, where there were ten.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-same-eight-loops-again\">The same eight loops, again<a href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/#the-same-eight-loops-again\" class=\"hash-link\" aria-label=\"Direct link to The same eight loops, again\" title=\"Direct link to The same eight loops, again\" translate=\"no\">​</a></h2>\n<p>Identical command to before. One flag added:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--name</span><span class=\"token plain\"> burn2 </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--network</span><span class=\"token plain\"> none </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--memory</span><span class=\"token plain\"> 256m --pids-limit </span><span class=\"token number\" style=\"color:#36acaa\">64</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--cpus</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0.5</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --read-only </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--tmpfs</span><span class=\"token plain\"> /tmp:size</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">16m </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --cap-drop</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">ALL --security-opt no-new-privileges </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  python:3.12-alpine </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'for i in $(seq 8); do (while :; do :; done) &amp; done; sleep 8'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless stats --no-stream burn2</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Docker stats showing the burn2 container held at 52.82 percent CPU with the same memory, network and process figures as before\" src=\"https://development-wec.wiline.com/docs/assets/images/h9-burn-capped-7dff3d60a38ee6b89cb8667301faea0f.png\" width=\"665\" height=\"204\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">NAME    CPU %    MEM USAGE / LIMIT    NET I/O   PIDS</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">burn2   52.82%   1.242MiB / 256MiB    0B / 0B   9</span><br></div></code></pre></div></div>\n<p><strong>605.19% → 52.82%.</strong> Same eight infinite loops, same image, same everything. Half a core, as\nasked.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-you-should-take-from-this\">What you should take from this<a href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/#what-you-should-take-from-this\" class=\"hash-link\" aria-label=\"Direct link to What you should take from this\" title=\"Direct link to What you should take from this\" translate=\"no\">​</a></h2>\n<p><strong>A container is not a sandbox until you say what it may use.</strong> The default is generous in every\ndimension that matters, and nothing warns you, because an unrestricted container is a completely\nnormal thing to run.</p>\n<p><strong>Check your limits are applied, rather than assuming.</strong> The whole of this post exists because a\nflag that was silently unavailable looked identical to a flag that was working. <code>docker stats</code>\nunder a deliberate load takes a minute and tells you the truth.</p>\n<p><strong>Error messages point at the wrong layer.</strong> \"your kernel does not support CPU CFS scheduler\"\ndescribed a kernel that supports it fine. The real cause was a systemd default, two layers away,\ndocumented on a different website.</p>\n<p>And the honest limit: this sandbox contains <em>resources</em>. It does not make the code safe. It\ncannot tell a useful script from a hostile one, and a program that does something harmful within\n256 MB, half a core and no network will run exactly as written. Containment buys you a blast\nradius, not a judgement.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Run the model's own code without trusting it</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://genai.owasp.org/llmrisk/llm052025-improper-output-handling/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">OWASP — LLM05:2025 Improper Output Handling</a></li>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/containers/resource_constraints/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker — Runtime options with Memory, CPUs, and GPUs</a></li>\n<li class=\"\"><a href=\"https://rootlesscontaine.rs/getting-started/common/cgroup2/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Rootless Containers — cgroup v2 delegation</a></li>\n<li class=\"\"><a href=\"https://docs.kernel.org/admin-guide/cgroup-v2.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Linux kernel — Control Group v2</a></li>\n<li class=\"\"><a href=\"https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>systemd.resource-control(5)</code> — <code>Delegate=</code></a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/",
            "title": "Sandbox the code your agent writes, and prove every limit actually applied",
            "summary": "OWASP names running model-generated code through exec or eval as its own vulnerability, tells you to treat the model as untrusted — and says nothing about how to contain it. So we built the containment: no network, 256 MB, 64 processes, read-only disk, no capabilities. Every one of those held. The CPU limit was refused outright, and eight busy loops took 605% of the host. The cause is one line of systemd configuration nobody mentions, and the fix needed no reboot despite the documentation insisting otherwise.",
            "date_modified": "2026-09-17T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "security",
                "docker",
                "sandbox",
                "agents",
                "code-execution",
                "cgroups",
                "self-hosting",
                "hardening"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/",
            "content_html": "<div class=\"tutorialHero\"><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">8</span><span class=\"skillTracker__skill\" data-state=\"current\">Move the daemon off root</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Here is the part nobody says out loud when they tell you to add yourself to the <code>docker</code> group\nso you can stop typing <code>sudo</code>.</p>\n<p>That group is root. Not \"close to root\", not \"root for Docker things\". If you can run a\ncontainer, you can read, change or delete any file on the machine — including the password\nfile, including other people's home directories, including the files the administrator\ndeliberately kept away from you.</p>\n<p>No exploit required. It is one command, and it takes about four seconds.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>One WEC Instance: 8 vCPU AMD EPYC 7601, 15 GB RAM, Ubuntu 22.04, kernel 5.15. System daemon is\nUbuntu's <code>docker.io</code> <strong>29.1.3</strong>; the rootless daemon installed here is <strong>29.8.1</strong> with\n<code>rootlesskit</code> 3.1.0. Test image is <code>alpine:3.20</code>. The host was running 20 containers under the\nsystem daemon throughout, and none of them stopped.</p><p>Every command output and timing below is from that run.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"proving-it-on-your-own-machine\">Proving it on your own machine<a href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/#proving-it-on-your-own-machine\" class=\"hash-link\" aria-label=\"Direct link to Proving it on your own machine\" title=\"Direct link to Proving it on your own machine\" translate=\"no\">​</a></h2>\n<p>Run this as a normal user. There is no <code>sudo</code> anywhere in it.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-v</span><span class=\"token plain\"> /:/host alpine:3.20 </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'touch /host/root/rootless-demo &amp;&amp; echo WROTE as uid $(id -u)'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">ls</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-l</span><span class=\"token plain\"> /root/rootless-demo</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"An unprivileged user runs a container that creates a file inside /root, and the host listing confirms the file is owned by root\" src=\"https://development-wec.wiline.com/docs/assets/images/h8-docker-group-is-root-8007f44ffccf255e2de1e18d9bf1ca00.png\" width=\"1158\" height=\"178\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WROTE as uid 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-rw-r--r-- 1 root root 0 Sep 16 20:58 /root/rootless-demo</span><br></div></code></pre></div></div>\n<p>A user who cannot so much as list <code>/root</code> just created a file in it, owned by root.</p>\n<p>Nothing malfunctioned. That is the design. The daemon runs as root, you asked it to mount the\nwhole filesystem, and it did — because a root process is allowed to. The container's \"root\" is\nthe host's root. They are the same thing.</p>\n<p>Docker documents this plainly, and it is worth reading their wording rather than mine. From\n<a href=\"https://docs.docker.com/engine/security/#docker-daemon-attack-surface\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker daemon attack surface</a>:</p>\n<blockquote>\n<p>only trusted users should be allowed to control your Docker daemon</p>\n</blockquote>\n<p>and, on the same page:</p>\n<blockquote>\n<p>Docker allows you to share a directory between the Docker host and a guest container; and it\nallows you to do so without limiting the access rights of the container.</p>\n</blockquote>\n<p>That is not a warning about a flaw. It is a description of the feature we just used.</p>\n<p>Clean it up before moving on:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-f</span><span class=\"token plain\"> /root/rootless-demo</span><br></div></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"why-this-is-worse-on-a-host-running-agents\">Why this is worse on a host running agents<a href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/#why-this-is-worse-on-a-host-running-agents\" class=\"hash-link\" aria-label=\"Direct link to Why this is worse on a host running agents\" title=\"Direct link to Why this is worse on a host running agents\" translate=\"no\">​</a></h3>\n<p>An AI agent that executes code it generated is, in security terms, remote code execution that\nyou installed on purpose. That is not a criticism of agents — it is what they are for.</p>\n<p>So the question stops being <em>\"could an attacker run code here?\"</em> — something already does,\nconstantly — and becomes <em>\"what can that code reach?\"</em></p>\n<p>If it can reach the Docker socket, the answer is everything, via the command above. Your API\nkeys. Your SSH keys. The database volume. All of it, regardless of how carefully the agent's\nown container was configured.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-part-2-did-and-did-not-fix\">What Part 2 did and did not fix<a href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/#what-part-2-did-and-did-not-fix\" class=\"hash-link\" aria-label=\"Direct link to What Part 2 did and did not fix\" title=\"Direct link to What Part 2 did and did not fix\" translate=\"no\">​</a></h3>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/\">Part 2</a> found two of six Langfuse containers\nrunning as root and fixed them with <code>user: \"999:999\"</code>. That was worth doing and it is still\nworth doing.</p>\n<p>But it changes who the process is <strong>inside</strong> the container. The daemon that starts it is still\nroot, and the socket is still a door to the whole machine. Part 2 hardened the passenger. This\npost goes after the driver.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-rootless-mode-actually-does\">What rootless mode actually does<a href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/#what-rootless-mode-actually-does\" class=\"hash-link\" aria-label=\"Direct link to What rootless mode actually does\" title=\"Direct link to What rootless mode actually does\" translate=\"no\">​</a></h2>\n<p>The daemon stops running as root and runs as you.</p>\n<p>Containers still think they have root — software inside them keeps working normally. But that\n\"root\" is now translated, by the kernel, into an ordinary unprivileged account that owns\nnothing important. Escape the container and you are not the administrator. You are <code>ubuntu</code>.</p>\n<p>The translation uses a block of user IDs the system set aside for you. Here is the whole idea\nin one picture — the same container, under each daemon:</p>\n<!-- -->\n<p>The left half is why the first command worked. The right half is what we are about to build,\nand the arithmetic in it is something you can verify yourself in a minute.</p>\n<p>This translation is a Linux kernel feature called a\n<a href=\"https://man7.org/linux/man-pages/man7/user_namespaces.7.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">user namespace</a>, not a Docker\ninvention — Docker is just asking the kernel for one. The block of IDs it maps into is declared\nin <a href=\"https://man7.org/linux/man-pages/man5/subuid.5.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>/etc/subuid</code></a>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'^ubuntu:'</span><span class=\"token plain\"> /etc/subuid /etc/subgid</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">/etc/subuid:ubuntu:100000:65536</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">/etc/subgid:ubuntu:100000:65536</span><br></div></code></pre></div></div>\n<p>65,536 IDs starting at 100000, yours alone. Hold onto that number — the mapping becomes\ncheckable arithmetic in a moment.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"installing-it-four-walls\">Installing it: four walls<a href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/#installing-it-four-walls\" class=\"hash-link\" aria-label=\"Direct link to Installing it: four walls\" title=\"Direct link to Installing it: four walls\" translate=\"no\">​</a></h2>\n<p>The documentation assumes a setup this machine does not have. Here is each wall in the order\nyou hit it.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"wall-1--the-package-does-not-exist\">Wall 1 — the package does not exist<a href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/#wall-1--the-package-does-not-exist\" class=\"hash-link\" aria-label=\"Direct link to Wall 1 — the package does not exist\" title=\"Direct link to Wall 1 — the package does not exist\" translate=\"no\">​</a></h3>\n<p>Every guide opens with the same line. On Ubuntu's Docker:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">apt-get</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">install</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> docker-ce-rootless-extras</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">dpkg </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-L</span><span class=\"token plain\"> docker.io </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-iE</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'rootless|rootlesskit'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"apt reports it cannot locate package docker-ce-rootless-extras, and listing the docker.io package contents for rootless files returns nothing\" src=\"https://development-wec.wiline.com/docs/assets/images/h8-no-rootless-package-4540e2d1cdbee668a23f37364afdc535.png\" width=\"1190\" height=\"246\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">E: Unable to locate package docker-ce-rootless-extras</span><br></div></code></pre></div></div>\n<p>That package belongs to Docker's own repository. This host runs Ubuntu's <code>docker.io</code>, and the\nsecond command returns <strong>nothing at all</strong> — the distribution package ships no rootless tooling\nwhatsoever.</p>\n<p>That matters because it forces a choice. You can replace <code>docker.io</code> with Docker CE, which\nmeans swapping out the daemon currently running your containers. Or you can install a second,\nuser-owned daemon beside the first. With 20 services running, the second option is the only\nsane one, and it is what the rest of this post does.</p>\n<p>What you do need from apt is the ID-mapping helpers:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">apt-get</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">install</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> uidmap</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>If apt offers to restart services</div><div class=\"admonitionContent_BuS1\"><p>On Ubuntu 22.04 this may trigger a \"Daemons using outdated libraries\" prompt with\n<code>containerd.service</code> and your database pre-selected. <code>uidmap</code> does not need any of them\nrestarted. Deselect them, or cancel the prompt — the install still completes.</p></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"wall-2--the-installer-refuses-to-run\">Wall 2 — the installer refuses to run<a href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/#wall-2--the-installer-refuses-to-run\" class=\"hash-link\" aria-label=\"Direct link to Wall 2 — the installer refuses to run\" title=\"Direct link to Wall 2 — the installer refuses to run\" translate=\"no\">​</a></h3>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-fsSL</span><span class=\"token plain\"> https://get.docker.com/rootless </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># Installing stable version 29.8.1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Aborting because rootful Docker is running and accessible.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Set FORCE_ROOTLESS_INSTALL=1 to ignore.</span><br></div></code></pre></div></div>\n<p>The guard assumes you are <em>migrating</em> — that a running root daemon means you are about to\nmake a mess. Running both side by side on purpose is never mentioned, and the way to proceed\nappears only inside the error text.</p>\n<p>Note the version it names: <strong>29.8.1</strong>, while the system daemon is <strong>29.1.3</strong>. You will end up\nwith two different Docker versions on one host. That is expected here, and worth knowing.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-fsSL</span><span class=\"token plain\"> https://get.docker.com/rootless </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">FORCE_ROOTLESS_INSTALL</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><br></div></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"wall-3--it-moves-your-cli-without-telling-you\">Wall 3 — it moves your CLI without telling you<a href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/#wall-3--it-moves-your-cli-without-telling-you\" class=\"hash-link\" aria-label=\"Direct link to Wall 3 — it moves your CLI without telling you\" title=\"Direct link to Wall 3 — it moves your CLI without telling you\" translate=\"no\">​</a></h3>\n<p>Read the last lines of the installer output:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[INFO] Creating CLI context \"rootless\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[INFO] Using CLI context \"rootless\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Current context is now \"rootless\"</span><br></div></code></pre></div></div>\n<p>Now ask Docker what is running:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> context </span><span class=\"token function\" style=\"color:#d73a49\">ls</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> default </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-q</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">wc</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-l</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker ps lists no containers at all, docker context ls shows the star on rootless, and querying the default context reports 20 running containers\" src=\"https://development-wec.wiline.com/docs/assets/images/h8-context-hijack-4d422c845ec7d88ee2b2f5a5ffbed4af.png\" width=\"1182\" height=\"330\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">NAME         DESCRIPTION                              DOCKER ENDPOINT                     ERROR</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">default      Current DOCKER_HOST based configuration  unix:///var/run/docker.sock</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">rootless *   Rootless mode                            unix:///run/user/1000/docker.sock</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">20</span><br></div></code></pre></div></div>\n<p>An empty list, while twenty containers run perfectly well. Nothing broke and nothing warned\nyou — the CLI is answering a different question than the one you think you asked, because the\ninstaller repointed it on the way out.</p>\n<p>Put it back, and opt in per command instead:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> context use default          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># everyday work, unchanged</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\">               </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># explicit when you want the new one</span><br></div></code></pre></div></div>\n<p>Start the daemon and confirm what you have:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable environment constant\" style=\"color:#36acaa\">XDG_RUNTIME_DIR</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">/run/user/</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">id</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-u</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">systemctl </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--user</span><span class=\"token plain\"> start docker.service</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless info </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--format</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{{.ServerVersion}} {{.SecurityOptions}}'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">29.8.1 [name=seccomp,profile=builtin name=rootless name=cgroupns]</span><br></div></code></pre></div></div>\n<p><code>name=rootless</code> is the daemon confirming what it is.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>The PATH advice is only half needed</div><div class=\"admonitionContent_BuS1\"><p>The installer tells you to add <code>~/bin</code> to your <code>PATH</code>. For day-to-day use you do not have to:\nthe system <code>docker</code> client talks to the rootless daemon perfectly well through the context, and\nevery command in this post was run that way. You need the PATH only for the setup and uninstall\nscripts — and without it they fail with <code>dockerd-rootless.sh needs to be present under $PATH</code>,\nwhich tells you nothing about what is actually wrong.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"checking-that-the-mapping-is-real\">Checking that the mapping is real<a href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/#checking-that-the-mapping-is-real\" class=\"hash-link\" aria-label=\"Direct link to Checking that the mapping is real\" title=\"Direct link to Checking that the mapping is real\" translate=\"no\">​</a></h2>\n<p>Ask a container who it is, then look at that same process from outside:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> alpine:3.20 </span><span class=\"token function\" style=\"color:#d73a49\">id</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--name</span><span class=\"token plain\"> u0 alpine:3.20 </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">60</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">user</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">,uid</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">,cmd</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-C</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'sleep 60'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--name</span><span class=\"token plain\"> u1000 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--user</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1000</span><span class=\"token plain\">:1000 alpine:3.20 </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">120</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">uid</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">,cmd</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-C</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'sleep 120'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The container reports uid 0 root; the host shows that process owned by ubuntu uid 1000; a container running as uid 1000 appears on the host as uid 100999\" src=\"https://development-wec.wiline.com/docs/assets/images/h8-uid-mapping-d53136aff71ace37ef98f708404f45d5.png\" width=\"1186\" height=\"648\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">uid=0(root) gid=0(root) groups=0(root),...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ubuntu    1000 sleep 60</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">100999 sleep 120</span><br></div></code></pre></div></div>\n<p>Read those three lines together, because they are the entire mechanism:</p>\n<ul>\n<li class=\"\">The container believes it is root.</li>\n<li class=\"\">The host says that process belongs to <code>ubuntu</code>.</li>\n<li class=\"\">A container running as ID 1000 shows up on the host as <strong>100999</strong>.</li>\n</ul>\n<p>That last number is <code>100000 + 1000 - 1</code> — the start of your reserved block, offset by the\ncontainer's own ID, because container root already took the first slot by mapping to you.</p>\n<p>Which gives you a test rather than a promise. Work it out from your own <code>/etc/subuid</code> line and\nsee whether the number matches. <strong>If you instead see plain <code>1000</code>, the translation is not\nhappening</strong> and something is wrong with the setup.</p>\n<p>Clean up:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless </span><span class=\"token function\" style=\"color:#d73a49\">rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-f</span><span class=\"token plain\"> u0 u1000</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-same-command-the-other-daemon\">The same command, the other daemon<a href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/#the-same-command-the-other-daemon\" class=\"hash-link\" aria-label=\"Direct link to The same command, the other daemon\" title=\"Direct link to The same command, the other daemon\" translate=\"no\">​</a></h2>\n<p>Back to the command this post opened with. Same image, same mount of the entire filesystem,\nsame flags. Only the daemon differs:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-v</span><span class=\"token plain\"> /:/host alpine:3.20 </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'touch /host/root/rootless-demo || echo DENIED'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The identical command that previously wrote into /root now fails with permission denied under the rootless daemon\" src=\"https://development-wec.wiline.com/docs/assets/images/h8-denied-e38f44dfdd6d58bf3f217438f3a584a0.png\" width=\"1186\" height=\"372\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">touch: /host/root/rootless-demo: Permission denied</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">DENIED</span><br></div></code></pre></div></div>\n<p>The container still believes it is root. The kernel disagrees, because the process behind it\nbelongs to <code>ubuntu</code>, and <code>ubuntu</code> cannot write to <code>/root</code>.</p>\n<p>That is the post, in two outputs.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-it-costs-you\">What it costs you<a href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/#what-it-costs-you\" class=\"hash-link\" aria-label=\"Direct link to What it costs you\" title=\"Direct link to What it costs you\" translate=\"no\">​</a></h2>\n<p>Four things, all measured here rather than quoted.</p>\n<p><strong>Containers do not start noticeably slower.</strong> Three consecutive runs:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">i</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"> /usr/bin/time </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-f</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"run+exit: %es\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> alpine:3.20 </span><span class=\"token boolean\" style=\"color:#36acaa\">true</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">80</span><span class=\"token plain\">:80 alpine:3.20 </span><span class=\"token boolean\" style=\"color:#36acaa\">true</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Three container runs completing in 1.19, 0.96 and 0.97 seconds, followed by the privileged port error refusing to expose port 80\" src=\"https://development-wec.wiline.com/docs/assets/images/h8-costs-03291fa4bb34544ae9eff02c34a5fa7a.png\" width=\"1190\" height=\"546\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">run+exit: 1.19s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">run+exit: 0.96s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">run+exit: 0.97s</span><br></div></code></pre></div></div>\n<p><strong>Ports below 1024 are refused</strong>, and unusually, the error tells you every way out:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">cannot expose privileged port 80, you can add</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">or set CAP_NET_BIND_SERVICE on rootlesskit binary,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">or choose a larger port number (&gt;= 1024)</span><br></div></code></pre></div></div>\n<p><code>-p 18080:80</code> binds without complaint. Behind a reverse proxy this rarely matters.</p>\n<p><strong>Disk throughput limits stop working.</strong> The daemon says so at startup, and it is easy to miss:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: No io.max (rbps) support</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: No io.max (wbps) support</span><br></div></code></pre></div></div>\n<p>So <code>--device-read-bps</code> and <code>--device-write-bps</code> silently do nothing. If you were relying on\nthose to stop one container starving the others, rootless takes that away.</p>\n<p><strong>Capability dropping still works</strong>, which matters because these are often treated as\nalternatives:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> --cap-drop</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">ALL alpine:3.20 </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'echo cap-drop ok'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">cap-drop ok</span><br></div></code></pre></div></div>\n<p>They stack. Keep using <code>user:</code> and <code>--cap-drop=ALL</code> — each layer assumes the one before it\nfailed.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"shared-folders-behave-differently\">Shared folders behave differently<a href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/#shared-folders-behave-differently\" class=\"hash-link\" aria-label=\"Direct link to Shared folders behave differently\" title=\"Direct link to Shared folders behave differently\" translate=\"no\">​</a></h2>\n<p>This is the part that catches people moving a real service across. Host permissions apply as\n<em>you</em>, not as root:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">D</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">/tmp/rl-vol</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$D</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> hello </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$D</span><span class=\"token plain\">/mine.txt</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"echo secret &gt; </span><span class=\"token string variable\" style=\"color:#36acaa\">$D</span><span class=\"token string\" style=\"color:#e3116c\">/theirs.txt; chmod 600 </span><span class=\"token string variable\" style=\"color:#36acaa\">$D</span><span class=\"token string\" style=\"color:#e3116c\">/theirs.txt\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-v</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$D</span><span class=\"token plain\">:/data alpine:3.20 </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token string\" style=\"color:#e3116c\">'cat /data/mine.txt; cat /data/theirs.txt 2&gt;&amp;1 || echo DENIED'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> rootless run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-v</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$D</span><span class=\"token plain\">:/data alpine:3.20 </span><span class=\"token function\" style=\"color:#d73a49\">stat</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'%u:%g %n'</span><span class=\"token plain\"> /data/mine.txt</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The container reads the user-owned file, is denied on the root-owned file, and reports the user-owned file as belonging to 0:0 inside the container\" src=\"https://development-wec.wiline.com/docs/assets/images/h8-volume-permissions-b16a91d894e4363082f108e17d3593e6.png\" width=\"1196\" height=\"374\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">hello</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">cat: can't open '/data/theirs.txt': Permission denied</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">DENIED</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">0:0 /data/mine.txt</span><br></div></code></pre></div></div>\n<p>Your own file reads fine. The root-owned one is refused — even though the container thinks it\nis root, because the kernel knows better. And your file appears inside the container as owned\nby <code>0:0</code>, the same translation running in reverse.</p>\n<p>Worth internalising before you move a database over and spend an hour wondering why it cannot\nopen its own data directory.</p>\n<p>One more: the rootless daemon keeps a <strong>separate image store</strong> at <code>~/.local/share/docker</code>.\nImages your system daemon already has are invisible to it, and everything gets pulled again. On\na tight disk, budget for it.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"wall-4--removing-it-refuses-too\">Wall 4 — removing it refuses too<a href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/#wall-4--removing-it-refuses-too\" class=\"hash-link\" aria-label=\"Direct link to Wall 4 — removing it refuses too\" title=\"Direct link to Wall 4 — removing it refuses too\" translate=\"no\">​</a></h2>\n<p>For symmetry, the uninstall has the same guard:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable environment constant\" style=\"color:#36acaa\">PATH</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">/home/ubuntu/bin:</span><span class=\"token environment constant\" style=\"color:#36acaa\">$PATH</span><span class=\"token plain\"> ~/bin/dockerd-rootless-setuptool.sh uninstall</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The uninstall script aborts because rootful Docker is running and accessible, instructing the user to set --force\" src=\"https://development-wec.wiline.com/docs/assets/images/h8-uninstall-refuses-9d0ed4f387ceb4f76df893f91156189a.png\" width=\"1188\" height=\"142\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[ERROR] Aborting because rootful Docker (/var/run/docker.sock) is running and accessible.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Set --force to ignore.</span><br></div></code></pre></div></div>\n<p>Add <code>--force</code> when you genuinely want it gone.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-this-does-and-does-not-buy-you\">What this does and does not buy you<a href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/#what-this-does-and-does-not-buy-you\" class=\"hash-link\" aria-label=\"Direct link to What this does and does not buy you\" title=\"Direct link to What this does and does not buy you\" translate=\"no\">​</a></h2>\n<p>It changes the worst case from <em>\"an attacker owns the machine\"</em> to <em>\"an attacker owns this\nuser account\"</em>. On a host where every service runs as <code>ubuntu</code>, that is a large move and not a\ncomplete one — your SSH keys, your <code>.env</code> files and your source code all still sit inside that\naccount.</p>\n<p>So the honest summary: rootless raises the floor. It does not make the room safe. Narrowing\nwhat the account itself can reach is the next rung, and that is where system-call filtering and\nimage supply chain come in.</p>\n<p>But the specific thing that was true at the top of this post — that anyone able to run a\ncontainer could take the whole machine — is no longer true, and you can prove it in one\ncommand.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Move the daemon off root</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/security/rootless/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker — Run the daemon as a non-root user (rootless mode)</a></li>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/security/userns-remap/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker — Isolate containers with a user namespace</a></li>\n<li class=\"\"><a href=\"https://github.com/rootless-containers/rootlesskit\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">rootlesskit</a></li>\n<li class=\"\"><a href=\"https://man7.org/linux/man-pages/man5/subuid.5.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>subuid(5)</code></a> and <a href=\"https://man7.org/linux/man-pages/man7/user_namespaces.7.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>user_namespaces(7)</code></a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/",
            "title": "Move the Docker daemon off root so a container escape lands on an ordinary user",
            "summary": "If you can run docker without sudo, you can already read every file on the machine. Not through a bug — that is how Docker is built. One command proves it. For a host running agents that execute generated code, that is the whole attack, and Part 2's non-root containers do not touch it. This moves the daemon itself to an unprivileged account, with the UID arithmetic you can check, four places the install walks you into a wall on Ubuntu, and what stops working afterwards.",
            "date_modified": "2026-09-16T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "security",
                "docker",
                "rootless",
                "containers",
                "privilege",
                "agents",
                "self-hosting",
                "hardening"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/",
            "content_html": "<div class=\"tutorialHero\"><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">7</span><span class=\"skillTracker__skill\" data-state=\"current\">Where the agent can go, not just what it can call</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/\">Agent orchestration part 4</a> split a toolbox so the\nscheduler could not issue refunds. <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/\">Part 5</a>\ngave the agent an identity at the gateway. <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/\">Part 6</a> put a JWT\nverifier in front of the tools server so it refuses anonymous callers.</p>\n<p>Every one of those controls <strong>what the agent may call</strong>. Not one of them controls <strong>where the\nagent may go</strong>.</p>\n<p>That distinction is the whole of this post. Exfiltration does not need a tool. It needs a\nsocket.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>One WEC Instance: 8 vCPU AMD EPYC 7601, 15 GB RAM, Ubuntu 22.04, kernel 5.15. Docker\n<code>29.1.3</code> with firewall backend <code>iptables</code> — which on this distribution is <code>iptables-nft</code>\nv1.8.7, a shim writing into nftables. Sandbox container is <code>alpine:3.20</code>. UFW is active.</p><p>Every status code, error string and timing in this post is from the run described. Plain\nHTTP on a private LAN where the LAN appears, as throughout this series.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"two-directions-and-only-one-is-usually-guarded\">Two directions, and only one is usually guarded<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#two-directions-and-only-one-is-usually-guarded\" class=\"hash-link\" aria-label=\"Direct link to Two directions, and only one is usually guarded\" title=\"Direct link to Two directions, and only one is usually guarded\" translate=\"no\">​</a></h2>\n<p>A firewall decides which packets may cross a boundary. Most of the time people mean it in one\ndirection — <strong>ingress</strong>, who out there may reach <em>in</em> to your service. That is what publishing\na port does, and what <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\">part 1 of this series</a>\nwas about.</p>\n<p><strong>Egress is the other direction</strong>: where your service may reach <em>out</em> to. Which addresses it\nmay open a connection to, and which it may not.</p>\n<p>Hardly anyone configures egress, and for an ordinary web application that is a reasonable\nchoice. It talks to its database and a payment provider, it does what its code says, and its\ncode does not change between deployments.</p>\n<p>An agent is not that. An agent decides what to do at runtime, from text it has just read — an\nemail, a support ticket, a web page, a document somebody else wrote. If an attacker controls\nthat text, they influence what the agent does next. And the thing you do not want it doing next\nis <strong>exfiltration</strong>: sending your data somewhere it should not go.</p>\n<p>Exfiltration needs no special tool. Any process that can open a socket can send anything it can\nread. Which is why the direction nobody guards is the one that matters here.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-the-standards-actually-say\">What the standards actually say<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#what-the-standards-actually-say\" class=\"hash-link\" aria-label=\"Direct link to What the standards actually say\" title=\"Direct link to What the standards actually say\" translate=\"no\">​</a></h2>\n<p>The leading reference for this class of problem is OWASP's <strong>LLM06:2025 Excessive Agency</strong>.\nIts prevention section lists, in order: <em>minimize extensions</em>, <em>minimize extension\nfunctionality</em>, <em>avoid open-ended extensions</em>, and further items about permissions and human\napproval.</p>\n<p>Read the page looking for the network and it is not there. Not \"egress\", not \"firewall\", not\n\"network\", not \"isolation\", not \"sandbox\" — the words do not appear in the entry at all.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The OWASP LLM06 Excessive Agency page showing its prevention and mitigation strategies, all concerning tools and extensions\" src=\"https://development-wec.wiline.com/docs/assets/images/h7-owasp-mitigations-d1025852bd1958d91bfe4304dd74c563.png\" width=\"2551\" height=\"1376\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Every mitigation on that list operates on <strong>tools</strong>. Remove the extension, narrow the\nextension, avoid the open-ended extension. All good advice. None of it changes a single byte\nof what a compromised process can do with a TCP socket.</p>\n<p>There is an argument that this is a gap rather than an oversight, and it is being made inside\nOWASP's own repository. An <a href=\"https://github.com/OWASP/www-project-top-10-for-large-language-model-applications/issues/802\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">open issue proposing a runtime-enforcement mapping</a>\nfor the Agentic Top 10 states it plainly:</p>\n<blockquote>\n<p>The enforcement pipeline assumes agents cannot reach tools without passing through the\nproxy. If tools are directly reachable, runtime enforcement collapses.</p>\n</blockquote>\n<p>and concludes that <em>\"Network-level controls (e.g., Kubernetes NetworkPolicy, Docker network\nisolation) are required to close this gap in production.\"</em></p>\n<p>Note what that is: an <strong>open issue</strong>, filed by a contributor, with its mapping marked\n<em>(Proposed)</em>. It is not the standard. It is someone arguing the standard should say this.</p>\n<p>And the reason this matters is not theoretical. <strong>EchoLeak</strong> (<a href=\"https://nvd.nist.gov/vuln/detail/CVE-2025-32711\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">CVE-2025-32711</a>)\nis a zero-click indirect prompt injection against Microsoft 365 Copilot, recorded in NVD as\n<em>\"Ai command injection in M365 Copilot allows an unauthorized attacker to disclose information\nover a network.\"</em> Over a network. The tool list was never the thing that failed.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">Docker on Linux with the <code>iptables</code> firewall backend — the default. Check with\n<code>docker info | grep -i \"Firewall Backend\"</code></li>\n<li class=\"\"><code>sudo</code>, and a willingness to write firewall rules on a box you can still reach</li>\n<li class=\"\">Nothing from the earlier parts. This one stands alone.</li>\n</ul>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Read this before you touch DOCKER-USER</div><div class=\"admonitionContent_BuS1\"><p><code>DOCKER-USER</code> is real firewall state on your host. A rule that matches more than you intended\nwill take services offline. Every rule in this post is scoped with <code>-i br-agent</code>, an interface\nthat belongs to one purpose-built network — which is exactly why we name that bridge by hand\nin step 1 instead of letting Docker generate one.</p><p>If you are working over SSH, none of these rules touch the <code>INPUT</code> chain, so your session is\nnot at risk. Confirm that for yourself before you believe it.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--prove-the-gap\">Step 1 — Prove the gap<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#step-1--prove-the-gap\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Prove the gap\" title=\"Direct link to Step 1 — Prove the gap\" translate=\"no\">​</a></h2>\n<p>A network, with the bridge interface named explicitly. Docker documents the option as\n<em>\"Interface name to use when creating the Linux bridge\"</em>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> network create </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--driver</span><span class=\"token plain\"> bridge </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">com.docker.network.bridge.name</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">br-agent agent-egress</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">ip</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-br</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">link</span><span class=\"token plain\"> show br-agent</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">br-agent         DOWN           8a:77:65:54:9b:0d &lt;NO-CARRIER,BROADCAST,MULTICAST,UP&gt;</span><br></div></code></pre></div></div>\n<p><strong>DOWN</strong>, because nothing is plugged into it yet. A Docker network is an interface that does\nnot come up until a container attaches, and a rule matching <code>-i br-agent</code> on an idle network\nsilently matches nothing. Worth knowing before you spend an hour debugging a policy that was\nnever exercised.</p>\n<p>The sandbox — pinned, not <code>latest</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--name</span><span class=\"token plain\"> agent-sandbox </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--network</span><span class=\"token plain\"> agent-egress alpine:3.20 </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> infinity</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> agent-sandbox apk </span><span class=\"token function\" style=\"color:#d73a49\">add</span><span class=\"token plain\"> --no-cache </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> bind-tools</span><br></div></code></pre></div></div>\n<p>Now four destinations. Note what is <em>not</em> in this container: no agent, no framework, no MCP\nclient, no tools. Tool scoping is not being bypassed here — there is nothing to bypass.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> agent-sandbox </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'inference API   HTTP %{http_code}\\n'</span><span class=\"token plain\"> https://inference.wiline.com/v1/models</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> agent-sandbox </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'arbitrary host  HTTP %{http_code}\\n'</span><span class=\"token plain\"> https://example.com</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> agent-sandbox </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'authentik LAN   HTTP %{http_code}\\n'</span><span class=\"token plain\"> http://10.80.4.212:9100/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> agent-sandbox </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'other network   exit=%{exitcode} %{errormsg}\\n'</span><span class=\"token plain\"> http://172.21.0.2:5432/</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">inference API   HTTP 401</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">arbitrary host  HTTP 200</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">authentik LAN   HTTP 302</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">other network   exit=28 Connection timed out after 5002 milliseconds</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Four probes from the sandbox container: the inference API answering 401, example.com answering 200, authentik answering 302, and a cross-network connection timing out\" src=\"https://development-wec.wiline.com/docs/assets/images/h7-before-unrestricted-527fc904745f99b3fdab1b9d08035428.png\" width=\"1234\" height=\"444\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>The <code>401</code> is the useful one to understand. No API key was sent, so the request never reaches\nanything that could charge for it — but a <code>401</code> is an HTTP response, which means the TCP\nhandshake completed, TLS negotiated, and a real server answered. Reachability, proven, for\nfree.</p>\n<p>The fourth line is the one that keeps this honest. That is a Postgres container on a different\nDocker network, and it <strong>times out</strong> — <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\">part 1 of this series</a>\nestablished that separate bridge networks genuinely isolate, and this confirms it.</p>\n<p>So the shape of the hole is precise rather than vague:</p>\n<table><thead><tr><th>Destination</th><th>Result</th><th></th></tr></thead><tbody><tr><td>Another Docker network</td><td>timeout</td><td>Docker already handles this</td></tr><tr><td>Your own LAN, including the identity provider</td><td><code>302</code></td><td>open</td></tr><tr><td>The open internet</td><td><code>200</code></td><td>open</td></tr><tr><td>The one API it actually needs</td><td><code>401</code></td><td>open</td></tr></tbody></table>\n<p>Docker isolates containers from <strong>each other</strong> and stops there. The two directions that matter\nfor exfiltration — your network, and the internet — are wide open, and no Docker guide frames\nthat as a gap because, for a web application, it isn't one.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--the-floor\">Step 2 — The floor<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#step-2--the-floor\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — The floor\" title=\"Direct link to Step 2 — The floor\" translate=\"no\">​</a></h2>\n<p>One rule. Note <code>-A</code> rather than <code>-I</code>: the deny goes at the <strong>bottom</strong> of the chain and stays\nthere, and every allow added later stacks above it. That ordering is the policy.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-A</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> br-agent </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> DROP</span><br></div></code></pre></div></div>\n<p><code>DOCKER-USER</code> is where this belongs, and Docker's documentation is explicit about why:</p>\n<blockquote>\n<p>Rules appended to the <code>FORWARD</code> chain will be processed after Docker's rules.</p>\n</blockquote>\n<p><code>DOCKER-USER</code> is processed <em>before</em> them — the docs describe it as <em>\"a placeholder for\nuser-defined rules that will be processed before rules in the <code>DOCKER-FORWARD</code> and <code>DOCKER</code>\nchains.\"</em> Anywhere else and Docker's own <code>ACCEPT</code> gets there first.</p>\n<!-- -->\n<p>Read that left to right and the placement explains itself. <code>DOCKER-FORWARD</code> holds one\nunconditional <code>ACCEPT</code> per bridge — that is what makes container egress work at all, and it is\nwhy a rule appended to <code>FORWARD</code> never fires. <code>DOCKER-USER</code> is the only hook that sits upstream\nof it.</p>\n<p>Re-run the same four probes:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">inference API   exit=28 Resolving timed out after 8000 milliseconds</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">arbitrary host  exit=28 Resolving timed out after 8002 milliseconds</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">authentik LAN   exit=28 Connection timed out after 8003 milliseconds</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">other network   exit=28 Connection timed out after 8002 milliseconds</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The single DROP rule listed, and all four probes failing with two distinct error messages\" src=\"https://development-wec.wiline.com/docs/assets/images/h7-default-deny-dd96eebe9dcfc37644f95bf30a45d1ae.png\" width=\"1234\" height=\"720\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Read the error text, not the failure.</strong> Two different messages, and the difference is free\ndiagnostic information:</p>\n<ul>\n<li class=\"\"><strong>\"Resolving timed out\"</strong> — DNS itself was blocked. We never learned an address.</li>\n<li class=\"\"><strong>\"Connection timed out\"</strong> — the address was already known, the packet left, the drop caught it.</li>\n</ul>\n<p>The two by-name probes died at resolution; the two by-IP probes died at the socket. Which\ntells us something not obvious: <strong>Docker's embedded resolver forwards its upstream queries\nacross <code>DOCKER-USER</code>.</strong> The resolver lives at <code>127.0.0.11</code> inside the container's namespace,\nand it is tempting to assume that traffic never touches the host's forward path. It does.</p>\n<p>Also note that every failure took the <strong>full eight seconds</strong>. A <code>DROP</code> produces timeouts; a\n<code>REJECT</code> fails instantly. Stealth against debuggability, and this post chooses stealth — but\nif you are still iterating on a policy, <code>REJECT</code> will save you a great deal of waiting.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--the-allowlist\">Step 3 — The allowlist<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#step-3--the-allowlist\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — The allowlist\" title=\"Direct link to Step 3 — The allowlist\" translate=\"no\">​</a></h2>\n<p>Before writing a rule for the resolver, find out which resolver the container actually uses:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> agent-sandbox </span><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> /etc/resolv.conf</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">nameserver 127.0.0.11</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">search corp.internal internal mesh.example.com</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">options edns0 trust-ad ndots:0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># Based on host file: '/etc/resolv.conf' (internal resolver)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># ExtServers: [8.8.8.8 8.8.4.4]</span><br></div></code></pre></div></div>\n<p><code>ExtServers: [8.8.8.8 8.8.4.4]</code>. <strong>The container is not using this network's DNS.</strong> The host\nresolves through <code>systemd-resolved</code> on <code>127.0.0.53</code>, which is a loopback address and therefore\nuseless inside a container's namespace, so Docker substituted Google Public DNS without\nmentioning it.</p>\n<p>Look at the inherited search domains in the same block (redacted here, but real on your box).\nEvery internal hostname that container looks up is being asked of <code>8.8.8.8</code>, suffixed with the\nprivate domains it inherited from the host. Nobody chose that, and nothing surfaces it except\nreading this file.</p>\n<p>It also decides the rule: allow the resolver the container <strong>has</strong>, not the one you assumed.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">getent hosts inference.wiline.com</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">67.207.107.229  inference.wiline.com</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-I</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> br-agent </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> udp </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--dport</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">53</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8.8</span><span class=\"token plain\">.8.8,8.8.4.4 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-I</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> br-agent </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> tcp </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--dport</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">53</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8.8</span><span class=\"token plain\">.8.8,8.8.4.4 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-I</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> br-agent </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> tcp </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--dport</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">443</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">67.207</span><span class=\"token plain\">.107.229 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> ACCEPT</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-S</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-N DOCKER-USER</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-A DOCKER-USER -d 67.207.107.229/32 -i br-agent -p tcp -m tcp --dport 443 -j ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-A DOCKER-USER -d 8.8.4.4/32 -i br-agent -p tcp -m tcp --dport 53 -j ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-A DOCKER-USER -d 8.8.8.8/32 -i br-agent -p tcp -m tcp --dport 53 -j ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-A DOCKER-USER -d 8.8.4.4/32 -i br-agent -p udp -m udp --dport 53 -j ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-A DOCKER-USER -d 8.8.8.8/32 -i br-agent -p udp -m udp --dport 53 -j ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-A DOCKER-USER -i br-agent -j DROP</span><br></div></code></pre></div></div>\n<p>Five allows on one deny. The same four probes, a third time:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">inference API   HTTP 401 exit=0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">arbitrary host  exit=28 Connection timed out after 8002 milliseconds</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">authentik LAN   exit=28 Connection timed out after 8003 milliseconds</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">other network   exit=28 Connection timed out after 8001 milliseconds</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The four probes after the allowlist: the inference API answering 401, and the other three destinations timing out at the connection stage rather than at DNS\" src=\"https://development-wec.wiline.com/docs/assets/images/h7-allowlist-applied-71e84e955ac296d1b262e120254f5409.png\" width=\"1240\" height=\"560\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Look at line two.</strong> It says <em>\"Connection timed out\"</em>, not <em>\"Resolving timed out\"</em>. DNS is\nopen, so <code>example.com</code> resolved to a real address — and then the packet died. The lookup\nsucceeded and the socket did not.</p>\n<p>That is the whole post in one line. A compromised process inside that container can discover\nwhere to send your data and cannot send it.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>The conntrack rule this doesn't need</div><div class=\"admonitionContent_BuS1\"><p>Every guide on this subject tells you to accept <code>RELATED,ESTABLISHED</code> traffic first, and\nDocker's documentation shows the rule. This policy has no such rule, and the <code>401</code> above\nproves the full TLS handshake and HTTP response came back anyway.</p><p>The reason is the direction. Our drop matches <code>-i br-agent</code> — traffic arriving <strong>from</strong> the\ncontainer. Return packets carry <code>-o br-agent</code>, never match it, pass through <code>DOCKER-USER</code>\nuntouched, and are accepted downstream by Docker's own connection-tracking rules.</p><p>You need the conntrack accept when your drop can match the return direction, which is the case\nin the ingress examples that advice comes from. A direction-specific egress drop does not. Add\nit anyway if you like — it costs nothing — but understand that copying it without understanding\nit is how people conclude firewall rules are black magic.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--measuring-the-cost-twice\">Step 4 — Measuring the cost, twice<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#step-4--measuring-the-cost-twice\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Measuring the cost, twice\" title=\"Direct link to Step 4 — Measuring the cost, twice\" translate=\"no\">​</a></h2>\n<p>A policy nobody measures is a policy someone will disable under load. So: what does six rules\ncost?</p>\n<p>The obvious approach, twenty connects with the rules on, then twenty with them off:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">i</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">seq</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable number\" style=\"color:#36acaa\">20</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> agent-sandbox </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'%{time_connect}\\n'</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8</span><span class=\"token plain\"> https://inference.wiline.com/v1/models</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sort</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-n</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">awk</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{a[NR]=$1} END{printf \"rules ON  — median %.4fs  min %.4fs  max %.4fs  (n=%d)\\n\", a[int(NR/2)+1], a[1], a[NR], NR}'</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-F</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">rules ON  — median 0.1573s  min 0.0940s  max 0.3768s  (n=20)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">rules OFF — median 0.0936s  min 0.0911s  max 0.1576s  (n=20)</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The naive measurement showing a 64 millisecond median difference between rules on and rules off\" src=\"https://development-wec.wiline.com/docs/assets/images/h7-overhead-naive-f360e40c29ad6c2b0c7ff16a3663104c.png\" width=\"1230\" height=\"376\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>64 milliseconds.</strong> Which would be a serious finding, if it were true.</p>\n<p>It is not. Six rules of packet matching is microseconds of kernel work, and the minimums give\nthe game away: <code>0.0940</code> against <code>0.0911</code>, near-identical. The fast path is the same in both\nruns. The entire difference lives in the tail.</p>\n<p><code>curl</code>'s <code>time_connect</code> is measured from the start of the request and therefore <strong>includes name\nresolution</strong>. The first run paid a full round trip to <code>8.8.8.8</code>. By the second, the resolver\nhad the answer cached. We measured the order we ran them in.</p>\n<p>Measure the TCP connect alone, by subtracting the lookup:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">i</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">seq</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable number\" style=\"color:#36acaa\">30</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> agent-sandbox </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'%{time_namelookup} %{time_connect}\\n'</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8</span><span class=\"token plain\"> https://inference.wiline.com/v1/models</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">awk</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{print ($2-$1)}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sort</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-n</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">awk</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{a[NR]=$1} END{printf \"connect-only median %.5fs  min %.5fs  max %.5fs  (n=%d)\\n\", a[int(NR/2)+1], a[1], a[NR], NR}'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">rules ON  — connect-only median 0.07098s  min 0.06999s  max 0.08607s  (n=30)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">rules OFF — connect-only median 0.07102s  min 0.06996s  max 0.07214s  (n=30)</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The corrected measurement showing a median difference of four hundredths of a millisecond, with the firewalled path nominally faster\" src=\"https://development-wec.wiline.com/docs/assets/images/h7-overhead-94f3b889647c2c273680e82c050b345a.png\" width=\"1240\" height=\"452\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>A median difference of −0.04 ms.</strong> Negative: the firewalled path measured marginally\n<em>faster</em>, which is what noise looks like. Everything sits inside about ±40 µs.</p>\n<p>So the honest answer is not a number with a plus sign in front of it. It is that the cost of\nthis policy is below what this method can resolve — and that the naive version of the same\nmeasurement was wrong by three orders of magnitude, in the direction that would have talked\nyou out of deploying it.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--surviving-a-reboot\">Step 5 — Surviving a reboot<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#step-5--surviving-a-reboot\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — Surviving a reboot\" title=\"Direct link to Step 5 — Surviving a reboot\" translate=\"no\">​</a></h2>\n<p><code>iptables</code> rules live in memory. That <code>-F</code> in the last step did to the policy exactly what a\nreboot does.</p>\n<p>The reflex is <code>iptables-persistent</code>, and it is the wrong tool here: it saves the whole table\nincluding every rule Docker generated, and Docker regenerates those itself at start. You get\nduplicates and an ordering nobody chose.</p>\n<p>What you want is a unit that owns <strong>only your rules</strong> and runs <strong>after</strong> Docker:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">/usr/local/sbin/agent-egress.sh</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token shebang important\">#!/usr/bin/env bash</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Egress allowlist for the agent bridge. Re-applied after Docker starts.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-euo</span><span class=\"token plain\"> pipefail</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">BR</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">br-agent</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">DNS</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"8.8.8.8 8.8.4.4\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">API</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">getent hosts inference.wiline.com </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">awk</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'{print $1; exit}'</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Remove only our own rules, so this is safe to run twice.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># `|| true` matters: on a clean chain grep exits 1 and would abort the script.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">existing</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">iptables </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-S</span><span class=\"token variable\" style=\"color:#36acaa\"> DOCKER-</span><span class=\"token variable environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">grep</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">\" -i </span><span class=\"token variable string variable\" style=\"color:#36acaa\">${BR}</span><span class=\"token variable string\" style=\"color:#e3116c\"> \"</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable operator\" style=\"color:#393A34\">||</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable boolean\" style=\"color:#36acaa\">true</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-n</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${existing}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">then</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">printf</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'%s\\n'</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${existing}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/^-A /-D /'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">while</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">read</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> rule</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"> iptables </span><span class=\"token variable\" style=\"color:#36acaa\">${rule}</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">||</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">true</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">fi</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">d</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">${DNS}</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-I</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${BR}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> udp </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--dport</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">53</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${d}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-I</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${BR}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> tcp </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--dport</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">53</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${d}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-I</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${BR}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> tcp </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--dport</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">443</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${API}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> ACCEPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-A</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">${BR}</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> DROP</span><br></div></code></pre></div></div>\n<div class=\"language-ini codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">/etc/systemd/system/agent-egress.service</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-ini codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[Unit]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Description=Egress allowlist for the agent bridge</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">After=docker.service</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Requires=docker.service</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[Service]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Type=oneshot</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">RemainAfterExit=yes</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ExecStart=/usr/local/sbin/agent-egress.sh</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[Install]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WantedBy=multi-user.target</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">chmod</span><span class=\"token plain\"> +x /usr/local/sbin/agent-egress.sh</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> systemctl daemon-reload </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> systemctl </span><span class=\"token builtin class-name\">enable</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--now</span><span class=\"token plain\"> agent-egress.service</span><br></div></code></pre></div></div>\n<p>Prove it is idempotent by restarting it and comparing the chain to itself:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> systemctl restart agent-egress.service </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-S</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The same six rules listed after two consecutive restarts of the unit, identical both times\" src=\"https://development-wec.wiline.com/docs/assets/images/h7-persistent-unit-08c4df2a28634e493a6a37476e9429fb.png\" width=\"1230\" height=\"610\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Six rules, twice, no duplicates.</p>\n<p>Two things this buys quietly. <code>getent</code> runs on every start, so <strong>the API address is re-resolved\nat boot</strong> — not continuous, but the cheap majority of the moving-target problem with no extra\ndaemon. And the delete loop strips only rules matching <code>-i br-agent</code>, so the unit never touches\nanything else in the chain.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can put a default-deny egress policy in front of a container, allow exactly the destinations\nit needs, tell a DNS failure apart from a socket failure by reading the error, measure the cost\nwithout fooling yourself, and make the whole thing survive a reboot.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"if-you-switch-docker-to-the-nftables-backend\">If you switch Docker to the nftables backend<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#if-you-switch-docker-to-the-nftables-backend\" class=\"hash-link\" aria-label=\"Direct link to If you switch Docker to the nftables backend\" title=\"Direct link to If you switch Docker to the nftables backend\" translate=\"no\">​</a></h2>\n<p>Docker 29 ships an nftables firewall backend alongside iptables. It is not the default and this\npost did not run it — the switch rebuilds every rule on the host, and verifying the full\nbehaviour needs a reboot. What follows is from Docker's documentation, flagged as such.</p>\n<p>Everything above stops applying, because:</p>\n<blockquote>\n<p>In Docker's nftables implementation, there is no <code>DOCKER-USER</code> chain.</p>\n</blockquote>\n<p>You create your own table instead, and the priority does the work <code>DOCKER-USER</code> used to:</p>\n<blockquote>\n<p>If your rules need to run before Docker's rules, give the base chains a lower priority number\nthan Docker's chain.</p>\n</blockquote>\n<p>And a rule that would be final under iptables is not:</p>\n<blockquote>\n<p>In nftables, an \"accept\" rule is not final. It terminates processing for its base chain, but\nthe accepted packet will still be processed by other base chains, which may drop it.</p>\n</blockquote>\n<p>Overriding Docker's drop then requires <code>--bridge-accept-fwmark</code>.</p>\n<p>The trap worth knowing about is the transition, and it is quiet in both directions:</p>\n<blockquote>\n<p>When starting the daemon with nftables after running with iptables, Docker will not remove the\njump from the <code>FORWARD</code> chain to <code>DOCKER-USER</code>. So, rules created in <code>DOCKER-USER</code> will\ncontinue to run until the jump is removed or the host is rebooted. When starting with nftables,\nthe daemon will not add the jump. So, unless there is an existing jump, rules in <code>DOCKER-USER</code>\nwill be ignored.</p>\n</blockquote>\n<p>Switch backends and your policy keeps working — until the next reboot, when it silently stops.\n<code>iptables -S DOCKER-USER</code> will still list every rule.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting--the-errors-this-run-actually-produced\">Troubleshooting — the errors this run actually produced<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#troubleshooting--the-errors-this-run-actually-produced\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting — the errors this run actually produced\" title=\"Direct link to Troubleshooting — the errors this run actually produced\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-systemd-unit-fails-on-its-first-run-and-works-on-the-second\">The systemd unit fails on its first run and works on the second<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#the-systemd-unit-fails-on-its-first-run-and-works-on-the-second\" class=\"hash-link\" aria-label=\"Direct link to The systemd unit fails on its first run and works on the second\" title=\"Direct link to The systemd unit fails on its first run and works on the second\" translate=\"no\">​</a></h3>\n<p><code>status=1/FAILURE</code> with nothing else in the journal. The cleanup line is the cause: on an empty\nchain, <code>grep</code> finds no matching rules and exits <code>1</code>, which <code>set -euo pipefail</code> turns into an\nabort before any rule is added. The idempotency guard breaks the one run that needs no\nidempotency. See the <code>|| true</code> in step 5.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"everything-times-out-including-things-you-allowed\">Everything times out, including things you allowed<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#everything-times-out-including-things-you-allowed\" class=\"hash-link\" aria-label=\"Direct link to Everything times out, including things you allowed\" title=\"Direct link to Everything times out, including things you allowed\" translate=\"no\">​</a></h3>\n<p>Check whether the failure says <em>\"Resolving\"</em> or <em>\"Connection\"</em>. If it says Resolving, your DNS\nrule is the problem and the destination rule was never reached. Containers frequently do not\nuse the resolver you expect — read <code>/etc/resolv.conf</code> inside the container, not on the host.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-rules-are-listed-but-nothing-is-filtered\">The rules are listed but nothing is filtered<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#the-rules-are-listed-but-nothing-is-filtered\" class=\"hash-link\" aria-label=\"Direct link to The rules are listed but nothing is filtered\" title=\"Direct link to The rules are listed but nothing is filtered\" translate=\"no\">​</a></h3>\n<p>Check that the bridge is up: <code>ip -br link show br-agent</code>. A network with no attached container\nhas a <code>DOWN</code> interface, and a rule matching <code>-i</code> on it matches nothing.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"everything-takes-exactly-eight-seconds-to-fail\">Everything takes exactly eight seconds to fail<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#everything-takes-exactly-eight-seconds-to-fail\" class=\"hash-link\" aria-label=\"Direct link to Everything takes exactly eight seconds to fail\" title=\"Direct link to Everything takes exactly eight seconds to fail\" translate=\"no\">​</a></h3>\n<p>That is <code>DROP</code> behaving correctly. Use <code>REJECT</code> while iterating if you would rather fail fast.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-measurement-shows-tens-of-milliseconds-of-overhead\">The measurement shows tens of milliseconds of overhead<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#the-measurement-shows-tens-of-milliseconds-of-overhead\" class=\"hash-link\" aria-label=\"Direct link to The measurement shows tens of milliseconds of overhead\" title=\"Direct link to The measurement shows tens of milliseconds of overhead\" translate=\"no\">​</a></h3>\n<p>You are measuring DNS. Use <code>%{time_connect}</code> minus <code>%{time_namelookup}</code>, and be suspicious of\nany result where the minimums match but the medians don't.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-this-did-and-didnt-buy-you\">What this did and didn't buy you<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#what-this-did-and-didnt-buy-you\" class=\"hash-link\" aria-label=\"Direct link to What this did and didn't buy you\" title=\"Direct link to What this did and didn't buy you\" translate=\"no\">​</a></h2>\n<p>Done: a container that can reach exactly one destination and a resolver; an exfiltration attempt\nthat fails after a successful DNS lookup rather than before it; a policy that reapplies itself\nafter a reboot and re-resolves its target when it does; and a measured cost indistinguishable\nfrom zero.</p>\n<p>Not done:</p>\n<ul>\n<li class=\"\"><strong>DNS is still an exfiltration channel.</strong> We allowed queries to <code>8.8.8.8</code>, and data can be\nsmuggled inside them. Closing that means a resolver you control and a rule pointing only at\nit — which is a different post.</li>\n<li class=\"\"><strong>The allowlist is an IP, and IP addresses move.</strong> The unit re-resolves at boot, which is not\nthe same as following a CDN. A forward proxy filtering on TLS SNI is the honest answer for a\ndestination that rotates; it is also considerably more machinery.</li>\n<li class=\"\"><strong>This protects containers.</strong> The agents in\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/\">agent orchestration part 3</a> and\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/\">part 4</a> run as host processes, where these rules do\nnot apply. Moving them into a container is the prerequisite, and is worth doing on its own\nmerits.</li>\n<li class=\"\"><strong>The host is unrestricted</strong>, and so is every other container on this box. Five other bridges\nstill have an unconditional <code>ACCEPT</code> in <code>DOCKER-FORWARD</code>.</li>\n<li class=\"\"><strong>Nothing here stops the injection.</strong> It stops the payload leaving. Those are different jobs.</li>\n</ul>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Where the agent can go, not just what it can call</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>The obvious sequel is the resolver: run one you control, point the container at it, allow port\n53 only to that address, and the DNS channel closes along with the dependency on Google. It also\nsets up domain-based allowlisting properly, which is the piece of the moving-target problem this\npost left open.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/network/packet-filtering-firewalls/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker — Packet filtering and firewalls</a></li>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/network/firewall-iptables/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker — Docker with iptables</a></li>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/network/firewall-nftables/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker — Docker with nftables</a></li>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/network/drivers/bridge/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker — Bridge network driver options</a></li>\n<li class=\"\"><a href=\"https://genai.owasp.org/llmrisk/llm062025-excessive-agency/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">OWASP — LLM06:2025 Excessive Agency</a></li>\n<li class=\"\"><a href=\"https://nvd.nist.gov/vuln/detail/CVE-2025-32711\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">NVD — CVE-2025-32711 (EchoLeak)</a></li>\n<li class=\"\"><a href=\"https://nvd.nist.gov/vuln/detail/CVE-2024-8309\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">NVD — CVE-2024-8309 (LangChain)</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/",
            "title": "Firewall an agent container so it can reach one API and nothing else",
            "summary": "Every part of this series so far has controlled what an agent may call. None of it controls where an agent may go. OWASP's mitigations for Excessive Agency are three rules about tools and say nothing about the network. This puts a default-deny egress policy in front of one container, allows exactly one API, and proves the boundary with a lookup that succeeds and a packet that dies. Includes the conntrack rule everyone tells you to add and this one doesn't need, and a measurement that was wrong by three orders of magnitude.",
            "date_modified": "2026-09-15T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "security",
                "docker",
                "networking",
                "iptables",
                "firewall",
                "agents",
                "self-hosting",
                "hardening"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__authentik\" src=\"data:image/png;base64,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\" alt=\"authentik\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__mcp\" src=\"data:image/png;base64,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\" alt=\"Model Context Protocol\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">6</span><span class=\"skillTracker__skill\" data-state=\"current\">A tool server that checks who is asking</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/\">Part 5</a> gave an agent its own identity at\nthe gateway: a token issued by authentik over client credentials, carrying a scope, expiring in\nfive minutes. It ended by naming what it had not covered — the MCP tools server from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/\">agent orchestration part 4</a> still trusts anything that\ncan reach its port, <code>issue_refund</code> included.</p>\n<p>That post was explicit about the limit of what it had built:</p>\n<blockquote>\n<p><strong>The MCP server still trusts everyone.</strong> Scoping happens in the <em>client</em>. Anything that can\nreach <code>127.0.0.1:8770</code> can call <code>issue_refund</code> directly, agent or not.</p>\n</blockquote>\n<p>Splitting the toolbox per role stopped an <em>agent</em> from reaching a tool it shouldn't. It did\nnothing about a <code>curl</code>. This part closes that.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>The same box as Parts 3 through 5: one WEC Instance, 8 vCPU AMD EPYC 7601, 15 GB RAM, Ubuntu\n22.04, Python 3.10. authentik <code>2026.8.1</code> on host port <code>9100</code>. <code>fastmcp</code> 4.0.2, <code>mcp</code> 2.1.1,\n<code>langchain</code> 1.4.0. The MCP server is <code>office_tools.py</code> from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/\">agent orchestration part 3</a>, unchanged except\nfor the lines shown here, still on <code>127.0.0.1:8770</code>.</p><p>Plain HTTP on a private LAN, as throughout this series.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"one-client-or-two\">One client or two<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/#one-client-or-two\" class=\"hash-link\" aria-label=\"Direct link to One client or two\" title=\"Direct link to One client or two\" translate=\"no\">​</a></h2>\n<p>The obvious move is to reuse Part 5's <code>agent-gateway</code> client and add a second scope to it. It\nalready exists, the agent already holds its credentials, and it would work.</p>\n<p>Don't. If one credential opens both the gateway and the tools server, then a leaked gateway key\nalso issues refunds, and the blast radius of losing it doubles. The whole thread running through\nPart 4's groups and Part 5's grant types is that a credential should do one job.</p>\n<p>So the tools server gets its own client, its own scope, and — because in authentik every\nprovider is its own issuer — its own issuer. That last part turns out to matter more than\nexpected, and we get to it in Step 4.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">The authentik from <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/\">Part 3</a>, with the <code>agent-gateway</code>\nclient from <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/\">Part 5</a></li>\n<li class=\"\">The MCP server and virtualenv from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/\">agent orchestration part 3</a></li>\n<li class=\"\"><code>fastmcp</code> 4.0.0 or newer — the client-credentials helper discussed in Step 5 does not exist\nbefore it</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--a-second-client-in-three-parts\">Step 1 — A second client, in three parts<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/#step-1--a-second-client-in-three-parts\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — A second client, in three parts\" title=\"Direct link to Step 1 — A second client, in three parts\" translate=\"no\">​</a></h2>\n<p>This is Part 5's procedure with different names, so it is deliberately terse here. If any of it\nis unfamiliar, <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/\">Part 5 walks through the same three screens</a>\nwith figures.</p>\n<p><strong>The provider.</strong> Applications → Providers → New Provider → OAuth2/OpenID → Next. Name\n<code>agent-tools</code>, Client Type <strong>Confidential</strong>, Redirect URIs empty, and in <strong>Grant Types</strong> uncheck\neverything except <strong>Client credentials</strong>.</p>\n<p><strong>The scope.</strong> Customization → Property Mappings → New Property Mapping → Scope Mapping. Mapping\nName <code>mcp-invoke</code>, Scope name <code>mcp:invoke</code>, expression <code>return {}</code>. The two name fields are\ndifferent things: the first is authentik's label, the second is the string that lands in the\ntoken.</p>\n<p><strong>The application.</strong> Applications → Applications → New Application ▾ → <strong>with Existing\nProvider…</strong>, named <code>Agent Tools</code>, slug <code>agent-tools</code> typed by hand, provider <code>agent-tools</code>,\nhidden from the dashboard. Then Edit the provider → Advanced protocol settings → Scopes, and\nmove <code>mcp-invoke</code> into Selected.</p>\n<p>Verify the grant types from the database rather than the form, which is\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/\">Part 4's lesson</a>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.providers.oauth2.models import OAuth2Provider</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for p in OAuth2Provider.objects.all():</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print(f'{p.name}: {p.grant_types}')\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-5</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Provider for Langfuse: ['authorization_code', 'implicit', 'urn:ietf:params:oauth:grant-type:device_code']</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Provider for LiteLLM: ['authorization_code', 'implicit', 'hybrid', 'refresh_token', 'client_credentials', 'password', 'urn:ietf:params:oauth:grant-type:device_code']</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">agent-gateway: ['client_credentials']</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">agent-tools: ['client_credentials']</span><br></div></code></pre></div></div>\n<p>Two clients trimmed to one grant each, and — visible in the same output — the LiteLLM provider\nfrom Part 4 still carrying <code>password</code> and five others nobody chose. Part 5 said to go back and\nfix those. This is what not doing it looks like.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--a-token-and-why-it-is-a-different-token\">Step 2 — A token, and why it is a different token<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/#step-2--a-token-and-why-it-is-a-different-token\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — A token, and why it is a different token\" title=\"Direct link to Step 2 — A token, and why it is a different token\" translate=\"no\">​</a></h2>\n<p>Read the credentials out and keep them beside the gateway's, in their own directory:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">read</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> TID TSEC </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-T</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.providers.oauth2.models import OAuth2Provider as P</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">p = P.objects.get(name='agent-tools')</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print(p.client_id, p.client_secret)\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> -1</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> ~/mcp-auth </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-auth</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">umask</span><span class=\"token plain\"> 077</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">AK_TOKEN_URL=http://10.80.4.212:9100/application/o/token/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">AK_CLIENT_ID=</span><span class=\"token string variable\" style=\"color:#36acaa\">$TID</span><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">AK_CLIENT_SECRET=</span><span class=\"token string variable\" style=\"color:#36acaa\">$TSEC</span><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">cp</span><span class=\"token plain\"> ~/agent-auth/token.sh ~/mcp-auth/token.sh</span><br></div></code></pre></div></div>\n<p>The helper is Part 5's, unchanged — it takes a scope and returns an access token.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/mcp-auth/token.sh mcp:invoke </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-R</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'split(\".\")[1] | gsub(\"-\";\"+\") | gsub(\"_\";\"/\") | @base64d | fromjson'</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"iss\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://10.80.4.212:9100/application/o/agent-tools/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"sub\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"545a532b77a0f877339a5e4312970069deeb9ded604494c510debdfdce9c970d\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"aud\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"xNxmOEyPMPVThmxgLsyvTCHa3qvnpVjQSOY0bZck\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"exp\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1789050030</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"iat\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1789049730</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"scope\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"mcp:invoke\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The decoded tools token showing an issuer ending in agent-tools, an audience different from the gateway client, and scope mcp\" src=\"https://development-wec.wiline.com/docs/assets/images/h6-tools-token-611a3b28adc48e09f38c168c63c7d1ac.png\" width=\"1108\" height=\"244\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Three fields differ from the gateway's token and all three are load-bearing. The <strong>issuer</strong> ends\n<code>/agent-tools/</code> rather than <code>/agent-gateway/</code>. The <strong>audience</strong> is a different client ID. The\n<strong>scope</strong> is <code>mcp:invoke</code>. <code>exp</code> minus <code>iat</code> is 300 seconds, same as Part 5 — remember that\nnumber, it comes back in Step 5.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Attaching a scope is not granting it</div><div class=\"admonitionContent_BuS1\"><p>If the token comes back with <code>\"scope\": \"\"</code>, the mapping exists but is not attached to the\nprovider. authentik's own help text on that pane: <em>\"Select which scopes can be used by the\nclient. The client still has to specify the scope to access the data.\"</em> Attaching makes it\navailable; the client still has to ask.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--the-verifier\">Step 3 — The verifier<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/#step-3--the-verifier\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — The verifier\" title=\"Direct link to Step 3 — The verifier\" translate=\"no\">​</a></h2>\n<p>FastMCP validates JWTs with <code>JWTVerifier</code>, which takes the four things authentik just told us.\nBack up the server first, since it is the artefact two published parts are built on:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cp</span><span class=\"token plain\"> ~/mcp-tools/office_tools.py ~/mcp-tools/office_tools.py.bak</span><br></div></code></pre></div></div>\n<p>Its configuration goes in a file rather than the source:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">umask</span><span class=\"token plain\"> 077</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> .env.mcp </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">MCP_JWKS_URI=http://10.80.4.212:9100/application/o/agent-tools/jwks/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">MCP_ISSUER=http://10.80.4.212:9100/application/o/agent-tools/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">MCP_AUDIENCE=&lt;the agent-tools client id&gt;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div></code></pre></div></div>\n<p>Then two changes to <code>office_tools.py</code> — an import, and a verifier where the bare constructor was:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/office_tools.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastmcp </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> FastMCP</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> Context</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastmcp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">server</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">auth</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">providers</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">jwt </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> JWTVerifier</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">verifier </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> JWTVerifier</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    jwks_uri</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"MCP_JWKS_URI\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    issuer</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"MCP_ISSUER\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    audience</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"MCP_AUDIENCE\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    required_scopes</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"mcp:invoke\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">mcp </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> FastMCP</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"office-tools\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> auth</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">verifier</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>That is the entire server-side change. <code>JWTVerifier</code> fetches the JWKS, matches each token's\n<code>kid</code> to a key, and checks signature, issuer, audience, expiry and scope before any tool runs.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span><code>ssrf_safe</code> and private addresses</div><div class=\"admonitionContent_BuS1\"><p><code>JWTVerifier</code> takes an <code>ssrf_safe</code> argument that defaults to <code>False</code>, and enabling it sounds\nunambiguously like a good idea. On a setup like this one it breaks the verifier. FastMCP's SSRF\nguard rejects on IP, and its own error text is <em>\"Private, loopback, link-local, and reserved IPs\nare not allowed\"</em> — the check explicitly covers <code>10.x</code>, <code>172.16-31.x</code>, <code>192.168.x</code> and <code>127.x</code>.\nOur JWKS is at <code>10.80.4.212</code>, so the fetch never leaves the process.</p><p>If you want the guard on and your identity provider is genuinely internal, the module reads\n<code>FASTMCP_SSRF_TRUST_PROXY</code>, which skips DNS resolution and the IP blocklist entirely.</p></div></div>\n<p>Restart with the new environment loaded:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">pkill</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-f</span><span class=\"token plain\"> office_tools.py</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-a</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> .env.mcp </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">nohup</span><span class=\"token plain\"> ./.venv/bin/python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-u</span><span class=\"token plain\"> office_tools.py </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> server.log </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">4</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-8</span><span class=\"token plain\"> server.log</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The server restarting, its startup log reporting transport and port with no mention of authentication\" src=\"https://development-wec.wiline.com/docs/assets/images/h6-server-auth-startup-e2aacc2f7033430c7a21caae3e07b195.png\" width=\"856\" height=\"326\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Read that startup log carefully, because of what is missing from it. It reports the transport,\nthe mode and the port, and says nothing whatsoever about authentication. A server with a\nverifier attached and a server without one log the same lines. There is no \"auth enabled\"\nmessage to look for, so the only way to know the verifier took is to make a request — which is\nthe next step.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--three-requests\">Step 4 — Three requests<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/#step-4--three-requests\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Three requests\" title=\"Direct link to Step 4 — Three requests\" translate=\"no\">​</a></h2>\n<p>The same shape as Part 5's three status codes, because it reads well and because the middle one\nis the interesting one:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">TOOLS</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/mcp-auth/token.sh mcp:invoke</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">GW</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/agent-auth/token.sh gateway:invoke</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function-name function\" style=\"color:#d73a49\">req</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$1</span><span class=\"token string\" style=\"color:#e3116c\">  HTTP %{http_code}</span><span class=\"token string entity\" style=\"color:#36acaa\">\\n</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST http://127.0.0.1:8770/mcp </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Accept: application/json, text/event-stream'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token variable\" style=\"color:#36acaa\">${2</span><span class=\"token variable operator\" style=\"color:#393A34\">:+</span><span class=\"token variable\" style=\"color:#36acaa\">-H \"Authorization</span><span class=\"token variable operator\" style=\"color:#393A34\">:</span><span class=\"token variable\" style=\"color:#36acaa\"> Bearer $2\"}</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">req </span><span class=\"token string\" style=\"color:#e3116c\">\"no token     \"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">req </span><span class=\"token string\" style=\"color:#e3116c\">\"gateway token\"</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$GW</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">req </span><span class=\"token string\" style=\"color:#e3116c\">\"tools token  \"</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$TOOLS</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">no token       HTTP 401</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">gateway token  HTTP 401</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">tools token    HTTP 200</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Three requests to the MCP server printing no token 401, gateway token 401 and tools token 200\" src=\"https://development-wec.wiline.com/docs/assets/images/h6-three-way-5268dcb96fa1731e359bf14df5c1b0e8.png\" width=\"633\" height=\"284\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>The first line is the gap agent orchestration part 4 admitted to, now closed. Before this change\nthat request returned the full five-tool catalogue to anybody who asked — and with the right\ntoken, it still does:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST http://127.0.0.1:8770/mcp </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$TOOLS</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Accept: application/json, text/event-stream'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tr</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'\\r'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-n</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/^data: //p'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'.result.tools | length, (.[].name)'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The tools list returned to an authorised caller: five tools, find_customer, list_availability, book_slot, open_invoices and issue_refund\" src=\"https://development-wec.wiline.com/docs/assets/images/h6-tools-list-authorised-e04041592339f5df452a50d4fc2cd30d.png\" width=\"665\" height=\"206\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Same five tools as before. Authentication changed who may ask, not what the server does.</p>\n<p>The second is the one worth pausing on. That is a <strong>valid, unexpired, correctly signed authentik\ntoken</strong> that works perfectly well against the gateway, and the tools server refuses it. Ask the\nserver why — sending the request again first, so the answer is at the end of the log rather\nthan buried under whatever has hit the server since:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">GW</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/agent-auth/token.sh gateway:invoke</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'gateway token: HTTP %{http_code}\\n'</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST http://127.0.0.1:8770/mcp </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$GW</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Accept: application/json, text/event-stream'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-12</span><span class=\"token plain\"> ~/mcp-tools/server.log</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING  Bearer token rejected for client aObZ8KvKsKVdQ4MuSsTINp0f65Puz5fzXUUBC6iQ</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">         : issuer mismatch (got 'http://10.80.4.212:9100/application/o/agent-gateway/',</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                       expected 'http://10.80.4.212:9100/application/o/agent-tools/')</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INFO     Auth error returned: invalid_token (status=401)</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The server log naming the rejected client and printing both the received and expected issuer, followed by a generic invalid_token response\" src=\"https://development-wec.wiline.com/docs/assets/images/h6-issuer-mismatch-log-67e89b1720edb880e97f3f7258d47aac.png\" width=\"690\" height=\"376\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>It failed on <code>iss</code>, not on scope.</strong> That is the separate provider paying off. In authentik each\nprovider is its own issuer, so a token from the wrong client is rejected at the first gate,\nbefore audience or scope are looked at. Had we taken the easy road and added a second scope to\nthe gateway's client, the same request would have got several checks further before\n<code>required_scopes</code> turned it away. Same outcome, more surface.</p>\n<p>Two other things in that log. It names the offending client by ID, and prints both the issuer it\ngot and the one it wanted — so debugging is a <code>tail</code>, not a guess. And the caller receives only\n<code>invalid_token</code>, which is correct: telling a client <em>which</em> issuer you expected is telling an\nattacker what to forge.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Don't grep this log</div><div class=\"admonitionContent_BuS1\"><p><code>grep -iE 'invalid|token|401' server.log</code> returns the last line and hides the first. You get\n<code>invalid_token (status=401)</code> and conclude the server won't say why. It says why, on the line\nabove, wrapped across four lines that your pattern doesn't match. Read it unfiltered.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--the-client-half-and-a-helper-that-cannot-help\">Step 5 — The client half, and a helper that cannot help<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/#step-5--the-client-half-and-a-helper-that-cannot-help\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — The client half, and a helper that cannot help\" title=\"Direct link to Step 5 — The client half, and a helper that cannot help\" translate=\"no\">​</a></h2>\n<p>The tools server is now protected, which means every script from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/\">part 3</a> and\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/\">part 4</a> is broken. They connect with no token.</p>\n<p>FastMCP documents exactly the right thing for this: <code>ClientCredentialsOAuthProvider</code>, which runs\nthe client-credentials grant itself and — the part that matters given our 300-second tokens —\n<em>\"when the token expires it is re-acquired automatically on the next request.\"</em></p>\n<p>It does not work here. Try it and you get:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">mcp.client.auth.exceptions.OAuthTokenError: Token exchange failed (404): Not Found</span><br></div></code></pre></div></div>\n<p>The server log explains what actually happened:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">GET  /.well-known/oauth-authorization-server      404</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">POST /token                                        404</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">GET  /.well-known/oauth-protected-resource         404</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">GET  /.well-known/oauth-protected-resource/mcp     404</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">GET  /.well-known/openid-configuration             404</span><br></div></code></pre></div></div>\n<p>The provider takes the <strong>MCP server's</strong> URL, not a token endpoint, because <em>\"the token endpoint\nis discovered from the server's OAuth metadata.\"</em> Our server publishes none — and FastMCP's own\ndocumentation says so, on a different page: <em>\"<code>TokenVerifier</code> focuses exclusively on token\nvalidation without providing OAuth discovery metadata.\"</em></p>\n<p>So the two pages are consistent and the combination is a documented dead end. You just have to\nread both to find that out, and the failure tells you none of it: four 404s you only see if you\nare watching the server, a fallback that POSTs <code>/token</code> at the resource server, and an error\nthat never mentions discovery.</p>\n<p>The path the docs point to instead is the client obtaining its token separately. <code>BearerAuth</code>\ntakes a token you already have and attaches it to every request — worth proving standalone\nbefore wiring it into four scripts:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"A probe script using BearerAuth with a token from the shell helper, printing all five discovered tool names\" src=\"https://development-wec.wiline.com/docs/assets/images/h6-client-bearer-works-5250c1328730dcadf4366a6843f881c0.png\" width=\"683\" height=\"427\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>That works, so it can be shared. One helper, used by every script in the series:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/mcp_auth.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"One authenticated MCP client, shared by every agent in the series.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> subprocess</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastmcp </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Client</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastmcp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">auth </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> BearerAuth</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MCP_URL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:8770/mcp\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">TOKEN_SH </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">path</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">expanduser</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"~/mcp-auth/token.sh\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">fetch_token</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">scope</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"mcp:invoke\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"Mint a short-lived token from authentik via the client credentials grant.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    out </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> subprocess</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">TOKEN_SH</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> scope</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> capture_output</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> text</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> check</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">stdout</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">strip</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">authed_client</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Client</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> Client</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">MCP_URL</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> auth</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">BearerAuth</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">token</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">fetch_token</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Each agent then changes by two lines — an import, and what it hands <code>MCPAdapter</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> mcp_auth </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> authed_client</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> MCPAdapter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">authed_client</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><br></div></code></pre></div></div>\n<p><code>MCPAdapter</code> accepts a configured <code>Client</code> as readily as a URL string, which\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/\">part 3 used for caching</a> without needing it\nfor anything else. Here it is what lets the token in.</p>\n<p><code>BearerAuth</code> is the explicit form. FastMCP also accepts the token as a bare string —\n<code>Client(url, auth=token)</code> — and adds the scheme itself; its documentation is specific that you\n<em>\"do not include the <code>Bearer</code> prefix\"</em> if you do. The class is worth the extra import here only\nbecause it makes the intent obvious at the call site.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>This reintroduces the expiry problem</div><div class=\"admonitionContent_BuS1\"><p><code>BearerAuth</code> holds one fixed string. A token minted when the agent starts is dead 300 seconds\nlater, and nothing re-acquires it — the automatic renewal was the one thing\n<code>ClientCredentialsOAuthProvider</code> would have given us. For a short run it does not matter. For an\nagent that waits on a human, as\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/\">part 3's refund does</a>, it certainly can. Either\nraise the token lifetime on the provider, or mint per call rather than per client.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-6--prove-a-tool-actually-runs\">Step 6 — Prove a tool actually runs<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/#step-6--prove-a-tool-actually-runs\" class=\"hash-link\" aria-label=\"Direct link to Step 6 — Prove a tool actually runs\" title=\"Direct link to Step 6 — Prove a tool actually runs\" translate=\"no\">​</a></h2>\n<p>Discovery is not execution. A token that lists tools might still fail at the call, so check the\nthing that touches the database:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">TOOLS</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/mcp-auth/token.sh mcp:invoke</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST http://127.0.0.1:8770/mcp </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$TOOLS</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Accept: application/json, text/event-stream'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"find_customer\",\"arguments\":{\"query\":\"Maria\"}}}'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tr</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'\\r'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-n</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/^data: //p'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.result.content[0].text // .error'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">\"[{\\\"id\\\":1,\\\"name\\\":\\\"Maria Alvarez\\\",\\\"email\\\":\\\"maria@example.com\\\"}]\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"A tools/call for find_customer, authenticated with a bearer token, returning Maria Alvarez&amp;#39;s row from the database\" src=\"https://development-wec.wiline.com/docs/assets/images/h6-tool-call-authorised-d181de9df9cf4ce219d07511a7b381d2.png\" width=\"841\" height=\"155\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Drop the <code>Authorization</code> header and the same call returns 401. A real row with a token, nothing\nwithout — that is authentication reaching execution, not just the catalogue.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can put a JWT verifier in front of an MCP tools server so it refuses anonymous callers and\nrefuses valid tokens issued to a different client — and read the server log to know which of\nissuer, audience, scope or expiry did the refusing.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting--the-errors-this-run-actually-produced\">Troubleshooting — the errors this run actually produced<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/#troubleshooting--the-errors-this-run-actually-produced\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting — the errors this run actually produced\" title=\"Direct link to Troubleshooting — the errors this run actually produced\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"token-exchange-failed-404-not-found\"><code>Token exchange failed (404): Not Found</code><a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/#token-exchange-failed-404-not-found\" class=\"hash-link\" aria-label=\"Direct link to token-exchange-failed-404-not-found\" title=\"Direct link to token-exchange-failed-404-not-found\" translate=\"no\">​</a></h3>\n<p><code>ClientCredentialsOAuthProvider</code> against a <code>JWTVerifier</code> server. Discovery found nothing and it\nfell back to guessing. See Step 5; use <code>BearerAuth</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"2-validation-errors--missing-required-argument\"><code>2 validation errors ... Missing required argument</code><a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/#2-validation-errors--missing-required-argument\" class=\"hash-link\" aria-label=\"Direct link to 2-validation-errors--missing-required-argument\" title=\"Direct link to 2-validation-errors--missing-required-argument\" translate=\"no\">​</a></h3>\n<p>The token was fine — this comes from inside the tool. <code>params.name</code> is the tool's name,\n<code>params.arguments</code> are its parameters, and both nest a field called <code>name</code>. <code>find_customer</code>\ntakes <code>query</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-log-says-only-invalid_token\">The log says only <code>invalid_token</code><a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/#the-log-says-only-invalid_token\" class=\"hash-link\" aria-label=\"Direct link to the-log-says-only-invalid_token\" title=\"Direct link to the-log-says-only-invalid_token\" translate=\"no\">​</a></h3>\n<p>You grepped it. See the warning in Step 4.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"every-script-from-parts-3-and-4-suddenly-fails\">Every script from parts 3 and 4 suddenly fails<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/#every-script-from-parts-3-and-4-suddenly-fails\" class=\"hash-link\" aria-label=\"Direct link to Every script from parts 3 and 4 suddenly fails\" title=\"Direct link to Every script from parts 3 and 4 suddenly fails\" translate=\"no\">​</a></h3>\n<p>Expected. They connect without a token. Step 5.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-this-did-and-didnt-buy-you\">What this did and didn't buy you<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/#what-this-did-and-didnt-buy-you\" class=\"hash-link\" aria-label=\"Direct link to What this did and didn't buy you\" title=\"Direct link to What this did and didn't buy you\" translate=\"no\">​</a></h2>\n<p>Done: an MCP server that authenticates every caller, a credential that is not the gateway's, a\nrefusal that happens at the issuer rather than deep in the scope check, and a log that names\nwhich client was turned away and why.</p>\n<p>Not done:</p>\n<ul>\n<li class=\"\"><strong>No per-tool authorisation, and this is less than agent orchestration part 4 promised.</strong> That\npost closed by saying the next step was to <em>\"authenticate the tool call, so the server knows\nwhich agent is asking and refuses <code>issue_refund</code> to anything that isn't billing.\"</em> Half of\nthat is now true: the call is authenticated. The other half is not. <code>required_scopes</code> is a\nproperty of the server, not of a tool, so every holder of <code>mcp:invoke</code> can call <code>issue_refund</code>\nexactly as easily as <code>find_customer</code>, and the server still cannot tell the scheduler from the\nbilling agent. Splitting tools per agent, as\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/\">part 4 did</a>, remains a client-side arrangement.</li>\n<li class=\"\"><strong>Tokens still expire mid-run</strong>, as described in Step 5.</li>\n<li class=\"\"><strong>Still plain HTTP.</strong> A bearer token in the clear is a credential in the clear.</li>\n<li class=\"\"><strong>The gateway's own provider is still over-permissioned</strong>, visible in Step 1's output and\nunfixed since Part 5 pointed at it.</li>\n</ul>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>A tool server that checks who is asking</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>Per-tool authorisation is the obvious gap and it does not have an obvious answer. <code>required_scopes</code>\ngates the server; gating <code>issue_refund</code> differently from <code>find_customer</code> means either a second\nserver with its own client, or authorisation inside each tool body reading the verified claims.\nThe first is more boxes and a clean boundary. The second is a check on every tool you ever write,\nwhich is precisely the failure mode\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/\">part 4 warned about</a> for policy-in-the-tool.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://gofastmcp.com/servers/auth/token-verification\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">FastMCP — token verification</a></li>\n<li class=\"\"><a href=\"https://gofastmcp.com/clients/auth/client-credentials\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">FastMCP — machine-to-machine client authentication</a></li>\n<li class=\"\"><a href=\"https://gofastmcp.com/clients/auth/bearer\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">FastMCP — bearer token authentication</a></li>\n<li class=\"\"><a href=\"https://docs.goauthentik.io/docs/providers/oauth2/client_credentials\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">authentik — client credentials</a></li>\n<li class=\"\"><a href=\"https://datatracker.ietf.org/doc/html/rfc9728\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">RFC 9728 — OAuth 2.0 Protected Resource Metadata</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/",
            "title": "The wrong valid token: authenticating an MCP tools server with authentik",
            "summary": "Agent orchestration part 4 scoped tools in the client and admitted the server still trusted whoever reached the port. This puts a JWT verifier in front of it, with its own authentik client, so the tools server refuses anonymous callers — and refuses the gateway's own valid token, because a separate provider means a separate issuer. Includes the documented client-credentials helper that cannot work here, and the four silent 404s it produces instead of saying so.",
            "date_modified": "2026-09-10T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "security",
                "oauth2",
                "oidc",
                "authentik",
                "identity",
                "mcp",
                "agents",
                "self-hosting",
                "hardening"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__authentik\" src=\"data:image/png;base64,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\" alt=\"authentik\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">5</span><span class=\"skillTracker__skill\" data-state=\"current\">An identity for the agent, not a key</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/\">Part 4</a> put group access control on the\ngateway's admin UI and then, at the end, called the API from a shell with no account, no\nsession and no group. It answered normally. The conclusion was that SSO protects a control\nplane and the data plane authenticates machine callers with keys — which it has to, because\nan agent running at 3am cannot complete a browser login.</p>\n<p>That was true and it was also a stopping point rather than an answer. \"Machine callers use\nkeys\" leaves you with a credential that never expires, that no identity provider knows about,\nand that survives the person who created it. Part 4 said so plainly: removing someone from a\ngroup does not revoke their keys, a leaked key is unaffected by identity entirely, and keys\noutlive people.</p>\n<p>This part gives the machine an identity instead of a key.</p>\n<p>authentik issues the agent a token over the <strong>client credentials</strong> grant — no browser, no\nconsent screen, no human. The token is signed, carries a scope, and expires in five minutes.\nThe gateway verifies it locally against authentik's public keys and refuses anything without\nthe right scope. At the end, three status codes show the boundary holding.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>One WEC Instance: 8 vCPU AMD EPYC 7601, 15 GB RAM, Docker 29.1.3, kernel 5.15. authentik\n<code>2026.8.1</code>, the same instance from Parts 3 and 4 on host port <code>9100</code>. LiteLLM <code>1.96.2</code> from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\">Part 1 of the gateway series</a>, bound to\n<code>127.0.0.1:4000</code>, models served over WEC Inference. Verification uses <code>PyJWT</code> 2.13.0 and\n<code>cryptography</code> 50.0.0, both already present in the LiteLLM image.</p><p>Plain HTTP on a private LAN, as in Parts 3 and 4. Substitute your own addresses.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"two-grants-two-different-questions\">Two grants, two different questions<a href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/#two-grants-two-different-questions\" class=\"hash-link\" aria-label=\"Direct link to Two grants, two different questions\" title=\"Direct link to Two grants, two different questions\" translate=\"no\">​</a></h2>\n<p>Parts 3 and 4 used the <strong>authorization code</strong> grant throughout. A person clicks a button, gets\nredirected to authentik, types a password, sees a consent screen, and is redirected back with\na code the application exchanges for a token. Every step of that assumes a browser and a human\nattached to it.</p>\n<p>The <strong>client credentials</strong> grant answers a different question. There is no user. The client\n<em>is</em> the principal, and it proves that with an ID and a secret, in one request, with no\nredirect anywhere.</p>\n<!-- -->\n<p>The property worth noticing: the gateway never asks authentik whether a token is good. It\nfetches authentik's public keys once, caches them, and checks the signature itself. Identity\nscales without the identity provider becoming a bottleneck — or a single point of failure on\nevery request.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">A working authentik, ideally the one from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/\">Part 3</a></li>\n<li class=\"\">A running LiteLLM gateway from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\">Part 1 of the gateway series</a></li>\n<li class=\"\">Its <code>LITELLM_MASTER_KEY</code>, and shell access to the box</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--a-client-with-exactly-one-grant\">Step 1 — A client with exactly one grant<a href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/#step-1--a-client-with-exactly-one-grant\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — A client with exactly one grant\" title=\"Direct link to Step 1 — A client with exactly one grant\" translate=\"no\">​</a></h2>\n<p><strong>Applications → Providers → New Provider → OAuth2/OpenID Provider → Next.</strong></p>\n<ul>\n<li class=\"\"><strong>Provider Name</strong> — <code>agent-gateway</code></li>\n<li class=\"\"><strong>Authorization Flow</strong> — leave <code>default-provider-authorization-explicit-consent</code>. It is\nrequired, and it is inert here: it governs the consent screen a human sees, and no human\nwill ever use this client. You have to pick one anyway.</li>\n<li class=\"\"><strong>Client Type</strong> — <strong>Confidential</strong>. authentik's own description is the reason: <em>\"Confidential\nclients are capable of maintaining the confidentiality of their credentials such as client\nsecrets.\"</em> Client credentials has nothing but that secret to prove identity, so a public\nclient cannot use this grant at all.</li>\n<li class=\"\"><strong>Redirect URIs</strong> — leave empty. Nothing is ever redirected anywhere.</li>\n</ul>\n<p>Then scroll to <strong>Grant Types</strong>, and look at what a new provider ships with.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Grant Types field on a new authentik provider with seven of eight boxes checked: Authorization Code, Implicit, Hybrid, Refresh token, Client credentials, Password and Device-code, with only Token exchange unchecked\" src=\"https://development-wec.wiline.com/docs/assets/images/h5-grant-types-default-a874ef714983fd8cd1c64bf2b966cb09.png\" width=\"1790\" height=\"837\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Seven of eight, enabled by default. Two of them — <strong>Implicit</strong> and <strong>Password</strong> — did not make\nit into OAuth 2.1, which states plainly that <em>\"some features available in OAuth 2.0, such as the\nImplicit or Resource Owner Credentials grant types, are not specified in OAuth 2.1\"</em>\n(<a href=\"https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13#section-10.1\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">§10.1</a>). Password\nin particular has the client handle a user's actual password, which is why it fell out of\nfavour. Nothing asked whether you wanted them and nothing warns you that they are on.</p>\n<p>Uncheck everything except <strong>Client credentials</strong>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The same Grant Types field with only Client credentials checked and the other seven boxes clear\" src=\"https://development-wec.wiline.com/docs/assets/images/h5-grant-types-scoped-d5992f14733c7ab87134b9a587a05c87.png\" width=\"1900\" height=\"915\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>This is the same least-privilege reflex Part 4 applied to group membership, one layer down. A\nclient that only ever uses one grant should only be permitted one grant, so that a\nmisconfiguration elsewhere cannot turn it into a password-accepting endpoint.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Go back and check the providers you already made</div><div class=\"admonitionContent_BuS1\"><p>Those defaults applied to every provider you created in Parts 3 and 4 too, and nothing has\ntrimmed them since. Ask the database rather than the UI:</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.providers.oauth2.models import OAuth2Provider</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for p in OAuth2Provider.objects.all():</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print(f'{p.name}: {p.grant_types}')\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-5</span><br></div></code></pre></div></div><div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Provider for Langfuse: ['authorization_code', 'implicit', 'urn:ietf:params:oauth:grant-type:device_code']</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Provider for LiteLLM: ['authorization_code', 'implicit', 'hybrid', 'refresh_token', 'client_credentials', 'password', 'urn:ietf:params:oauth:grant-type:device_code']</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">agent-gateway: ['client_credentials']</span><br></div></code></pre></div></div><p>The gateway's provider is still holding <code>password</code> and <code>client_credentials</code>\nalongside the grant it actually uses. Neither was needed, and the password grant in particular\nis a live way in that nobody put there on purpose. Trim each provider to the grants it uses.</p></div></div>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>An empty redirect list is not an empty rule</div><div class=\"admonitionContent_BuS1\"><p>Read authentik's own help text on the Redirect URIs field: <em>\"If no explicit authorization\nredirect URIs are specified, the first successfully used authorization redirect URI will be\nsaved.\"</em></p><p>Empty does not mean \"deny all\". It means trust-on-first-use — authentik pins whichever\nredirect URI shows up first and keeps it. That is harmless for a client which never redirects,\nwhich is our case. It is not harmless on an interactive provider, where it means you have no\nredirect validation at all until someone logs in and pins one for you. Part 3 warned against\nsetting the mode to <code>.*</code>; this is the quieter version of the same hazard.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--the-application-it-belongs-to\">Step 2 — The application it belongs to<a href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/#step-2--the-application-it-belongs-to\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — The application it belongs to\" title=\"Direct link to Step 2 — The application it belongs to\" translate=\"no\">​</a></h2>\n<p>Read back the credentials and try to get a token. It will not work yet, and the reason is the\nmost useful thing in this post.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> ~/agent-auth</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/agent-auth</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">umask</span><span class=\"token plain\"> 077</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">AK_TOKEN_URL=http://10.80.4.212:9100/application/o/token/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">AK_CLIENT_ID=&lt;client id from the provider page&gt;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">AK_CLIENT_SECRET=&lt;client secret from the provider page&gt;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div></code></pre></div></div>\n<p><code>umask 077</code> creates the file with 600 permissions rather than chmod-ing it afterwards, so there\nis no window where the secret is world-readable.</p>\n<p>A small helper, because you will fetch tokens constantly:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> ~/agent-auth/token.sh </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">#!/usr/bin/env bash</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">set -euo pipefail</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">source \"$(dirname \"$0\")/.env\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">curl -sS -X POST \"$AK_TOKEN_URL\" \\</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  -d grant_type=client_credentials \\</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  -d client_id=\"$AK_CLIENT_ID\" \\</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  -d client_secret=\"$AK_CLIENT_SECRET\" \\</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  -d scope=\"${1:-}\" \\</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  | jq -r .access_token</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">chmod</span><span class=\"token plain\"> +x ~/agent-auth/token.sh</span><br></div></code></pre></div></div>\n<p>Ask for a token:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/agent-auth</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> .env</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$AK_TOKEN_URL</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">grant_type</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">client_credentials </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">client_id</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$AK_CLIENT_ID</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">client_secret</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$AK_CLIENT_SECRET</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"error\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"invalid_grant\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"error_description\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"The provided authorization grant or refresh token is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"request_id\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"757c397b97a745259e288b6d4ff1553c\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>Every clause in that description is wrong for our situation. Nothing is expired, nothing is\nrevoked, no redirection URI was used, and the client is the one the credentials belong to. The\nserver log records the 400 and no reason. authentik's Events show nothing useful either.</p>\n<p>The actual cause is visible in the Providers list, which flags it plainly:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The authentik Providers list showing agent-gateway with a warning reading Provider not assigned to any application, beside two healthy providers assigned to Langfuse and LiteLLM\" src=\"https://development-wec.wiline.com/docs/assets/images/h5-provider-unassigned-f6f2e4435e386fcaa6db74ac117566a4.png\" width=\"1901\" height=\"830\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>A provider with no application cannot issue a token.</strong> authentik authorizes against the\n<em>application</em>, so a provider on its own has nothing to authorize against — and says\n<code>invalid_grant</code> instead of saying that.</p>\n<p>This is not in authentik's client credentials documentation, which documents five ways to\nauthenticate — three static-credential variants and two JWT ones — and says nothing about\nneeding an application.</p>\n<p>Pair one. Note the route: the <strong>New Application</strong> wizard always creates a <em>new</em> provider, so it\ncannot adopt the one we just built.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The New application wizard&amp;#39;s Configure the Application step, with its five-step list on the left reading Application, Choose a Provider, Configure Provider, Configure Bindings, Review and Submit\" src=\"https://development-wec.wiline.com/docs/assets/images/h5-application-created-4cf5cd7238257dde87681d0c92e34efd.png\" width=\"1894\" height=\"913\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Its step list gives it away — <strong>Choose a Provider</strong> is step 2, and every option there builds a\nnew one. The caret beside the button is where the other path lives.</p>\n<p><strong>Applications → Applications → New Application ▾ → with Existing Provider…</strong></p>\n<ul>\n<li class=\"\"><strong>Name</strong> — <code>Agent Gateway</code></li>\n<li class=\"\"><strong>Slug</strong> — <code>agent-gateway</code>, typed explicitly. Part 4's finding was that authentik derives\nslugs from names and turned <code>LiteLLM</code> into <code>lite-llm</code>; the slug lands in the issuer URL, so\nit is worth setting by hand every time.</li>\n<li class=\"\"><strong>Provider</strong> — <code>agent-gateway</code></li>\n<li class=\"\"><strong>Hidden from Application Dashboard</strong> — check it. No person should ever see a tile for a\nmachine client.</li>\n</ul>\n<p>Retry the token request and it succeeds. Nothing changed except the pairing.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--a-scope-that-means-something\">Step 3 — A scope that means something<a href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/#step-3--a-scope-that-means-something\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — A scope that means something\" title=\"Direct link to Step 3 — A scope that means something\" translate=\"no\">​</a></h2>\n<p>The token you now get carries <code>\"scope\": \"\"</code>. It proves <em>who</em> the caller is and grants no\nauthority at all. A gateway receiving it knows the client is <code>agent-gateway</code> and nothing more.</p>\n<p><strong>Customization → Property Mappings → New Property Mapping → Scope Mapping.</strong></p>\n<ul>\n<li class=\"\"><strong>Mapping Name</strong> — <code>gateway-invoke</code>, authentik's label for the object</li>\n<li class=\"\"><strong>Scope name</strong> — <code>gateway:invoke</code>, the string clients request and that appears in the token</li>\n<li class=\"\"><strong>Expression</strong> — <code>return {}</code></li>\n</ul>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Scope Mapping form with Mapping Name gateway-invoke, Scope name gateway, a description, and the expression return empty braces\" src=\"https://development-wec.wiline.com/docs/assets/images/h5-scope-mapping-93c6d51e31e1b4e0e089d72f023f4714.png\" width=\"1895\" height=\"867\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>The two name fields are different things and the form does not make that obvious. The\nexpression is Python whose return value is merged into the token's claims; an empty dict is\nright here, because we want the scope <em>named</em> in the token, not extra data carried inside it.</p>\n<p>Attach it: <strong>Applications → Providers → <code>agent-gateway</code> → Edit → Advanced protocol settings →\nScopes</strong>, and move <code>gateway-invoke</code> from Available to Selected.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The provider&amp;#39;s Scopes dual-list with email, openid, profile and gateway-invoke in the Selected column\" src=\"https://development-wec.wiline.com/docs/assets/images/h5-provider-scopes-1cf3161c12ea30bfd90af1bc227bb8d1.png\" width=\"1836\" height=\"841\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Read the help text under that pane, because it is the whole mechanic:</p>\n<blockquote>\n<p><em>\"Select which scopes can be used by the client. The client still has to specify the scope to\naccess the data.\"</em></p>\n</blockquote>\n<p>Attaching a scope makes it <em>available</em>. The client still has to ask. That is why the first\ntoken came back empty even though three scopes were already attached — nothing had requested\nthem.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Clicking a selected scope marks it for removal</div><div class=\"admonitionContent_BuS1\"><p>The dual-list marks entries for deletion when you click them in the right-hand column, and the\nonly indication is a line of small text above it reading <em>\"4 items selected. 1 item marked to\nremove.\"</em> Saving at that moment silently unlinks the scope you just added. Read that line\nbefore you save.</p></div></div>\n<p>Now ask for it:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">TOKEN</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/agent-auth/token.sh gateway:invoke</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-R</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'split(\".\")[1] | gsub(\"-\";\"+\") | gsub(\"_\";\"/\") | @base64d | fromjson'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;&lt;</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$TOKEN</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"iss\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://10.80.4.212:9100/application/o/agent-gateway/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"sub\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"2451c6a1bab3085590263f119e650448beb013336146bcfee8241dd5832c200b\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"aud\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"aObZ8KvKsKVdQ4MuSsTINp0f65Puz5fzXUUBC6iQ\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"exp\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1788901322</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"iat\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1788901022</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"acr\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"goauthentik.io/providers/oauth2/default\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"jti\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"NRDTukijwLJbW7gpPtn9zUZwzTj6d2kBSZCuFYvX\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"azp\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"aObZ8KvKsKVdQ4MuSsTINp0f65Puz5fzXUUBC6iQ\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"uid\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"neugHWoWQOQzADgbV1vKwzew2COQ1RgCXfyywgqa\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"scope\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"gateway:invoke\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The decoded JWT payload showing iss, sub, aud, exp, jti, uid and scope set to gateway\" src=\"https://development-wec.wiline.com/docs/assets/images/h5-token-scoped-83408e96e6123963ab117fedea8d70de.png\" width=\"695\" height=\"276\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><code>exp</code> minus <code>iat</code> is 300 seconds. That is the entire argument for this over a static key: a\ntoken pasted into a chat window, a log file or a screenshot is worthless five minutes later.</p>\n<p>There is no <code>email</code> or <code>preferred_username</code> in there, because there is no user. <code>sub</code> is a\nservice account authentik created for you, named <code>ak-agent-gateway-client_credentials</code> — it\nappears in <strong>Directory → Users</strong> without ever being asked for.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--teaching-the-gateway-to-verify\">Step 4 — Teaching the gateway to verify<a href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/#step-4--teaching-the-gateway-to-verify\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Teaching the gateway to verify\" title=\"Direct link to Step 4 — Teaching the gateway to verify\" translate=\"no\">​</a></h2>\n<p>LiteLLM documents JWT authentication with a <code>litellm_jwtauth</code> block, <code>enforce_scope_based_access</code>\nand <code>scope_mappings</code>. Configure it on a stock install and nothing happens, because the same page\nsays:</p>\n<blockquote>\n<p><em>\"JWT-based Auth requires a LiteLLM Enterprise license.\"</em></p>\n</blockquote>\n<p>Check before you write config:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from litellm.proxy.proxy_server import premium_user</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('premium_user =', premium_user)\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">premium_user = False</span><br></div></code></pre></div></div>\n<p><code>False</code> means that whole documented path is closed to you. The open-source alternative is\n<code>custom_auth</code>, which hands every request to a function you write — and for this job it is about\ntwenty-five lines.</p>\n<div class=\"theme-admonition theme-admonition-danger admonition_xJq3 alert alert--danger\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z\"></path></svg></span>custom_auth replaces key authentication entirely</div><div class=\"admonitionContent_BuS1\"><p>Once <code>custom_auth</code> is set, <em>every</em> request goes through your function, including the <code>sk-</code>\nvirtual keys from the rest of the gateway series and the master key itself. A handler that only\nunderstands JWTs locks you out of your own gateway. The handler below falls back to the master\nkey deliberately.</p><p>Back up before you start: <code>cp config.yaml config.yaml.bak</code></p></div></div>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/llm-gateway/jwt_auth.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> jwt</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastapi </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Request</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> litellm</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">proxy</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">_types </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> UserAPIKeyAuth</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">JWKS_URL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"JWT_PUBLIC_KEY_URL\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ISSUER </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"JWT_ISSUER\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AUDIENCE </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"JWT_AUDIENCE\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">REQUIRED_SCOPE </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"gateway:invoke\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MASTER_KEY </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"LITELLM_MASTER_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">_jwks </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> jwt</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">PyJWKClient</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">JWKS_URL</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">user_api_key_auth</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">request</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Request</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> api_key</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> UserAPIKeyAuth</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    token </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> api_key</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">removeprefix</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"Bearer \"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">strip</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Not a JWT: fall back to the master key so existing tooling keeps working.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> token</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">count</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\".\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">!=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> token </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> MASTER_KEY</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> UserAPIKeyAuth</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">token</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">raise</span><span class=\"token plain\"> Exception</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"not a JWT and not the master key\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    signing_key </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> _jwks</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get_signing_key_from_jwt</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">token</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    claims </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> jwt</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">decode</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        token</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        signing_key</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">key</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        algorithms</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"RS256\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        issuer</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">ISSUER</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        audience</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">AUDIENCE</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> REQUIRED_SCOPE </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> claims</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scope\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">split</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">raise</span><span class=\"token plain\"> Exception</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"token is missing scope </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">REQUIRED_SCOPE</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> UserAPIKeyAuth</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">token</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> user_id</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">claims</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"sub\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p><code>PyJWKClient</code> does the part that looks hard. It fetches the JWKS, matches the token's <code>kid</code>\nheader to the right key, and caches it — so after the first request, verification is local\narithmetic. <code>jwt.decode</code> checks the signature, the issuer, the audience and the expiry in one\ncall, and raises if any of them is wrong.</p>\n<p>Three environment variables, appended to <code>~/llm-gateway/.env</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">JWT_PUBLIC_KEY_URL</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">http://10.80.4.212:9100/application/o/agent-gateway/jwks/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">JWT_ISSUER</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">http://10.80.4.212:9100/application/o/agent-gateway/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">JWT_AUDIENCE</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">aObZ8KvKsKVdQ4MuSsTINp0f65Puz5fzXUUBC6iQ</span><br></div></code></pre></div></div>\n<p><code>JWT_ISSUER</code> and <code>JWT_AUDIENCE</code> must match the <code>iss</code> and <code>aud</code> you saw in the decoded token\nexactly, or every token is refused.</p>\n<p>Mount the module the same way the gateway already mounts <code>scrubber.py</code>, in\n<code>docker-compose.yml</code>:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./jwt_auth.py</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/jwt_auth.py</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div></code></pre></div></div>\n<p>And in <code>config.yaml</code>, under <code>general_settings</code>:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">custom_auth</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> jwt_auth.user_api_key_auth</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">custom_auth_run_common_checks</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:#36acaa\">true</span><br></div></code></pre></div></div>\n<p>That second line matters more than it looks. Without it, LiteLLM warns at startup:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The LiteLLM startup log, carrying the custom_auth_run_common_checks warning among the model registration warnings, ending in Application startup complete\" src=\"https://development-wec.wiline.com/docs/assets/images/h5-custom-auth-warning-863f445ce677a95c6cf806de7ecfc4c7.png\" width=\"942\" height=\"972\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<blockquote>\n<p><em>\"custom_auth is configured but 'custom_auth_run_common_checks' is not set. Problem: budgets,\nmodel-access allowlists, and per-model rate limits configured on your DB team/project records\nwill NOT be enforced for custom-auth requests.\"</em></p>\n</blockquote>\n<p>Read that carefully. Adding token authentication would, by default, have exempted every token\nholder from the controls you already configured. You would have made the gateway more secure in\none dimension and quietly weaker in another, and the only notice is one warning line at\nstartup.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Editing a mounted config does not restart anything</div><div class=\"admonitionContent_BuS1\"><p><code>config.yaml</code> is a bind mount. Changing its contents changes no part of the compose spec, so\n<code>docker compose up -d</code> reports <code>Container llm-gateway  Running</code> and does nothing. Your tests\nthen run against the old process and reproduce the old results exactly, which reads like your\nchange had no effect.</p><p>Use <code>docker restart llm-gateway</code>, and confirm by the warning above <em>disappearing</em> from the log.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--the-boundary-in-three-status-codes\">Step 5 — The boundary, in three status codes<a href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/#step-5--the-boundary-in-three-status-codes\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — The boundary, in three status codes\" title=\"Direct link to Step 5 — The boundary, in three status codes\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">TOKEN</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/agent-auth/token.sh gateway:invoke</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">NOSCOPE</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/agent-auth/token.sh</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'no token      HTTP %{http_code}\\n'</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-mid\",\"messages\":[{\"role\":\"user\",\"content\":\"Say OK.\"}]}'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'wrong scope   HTTP %{http_code}\\n'</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$NOSCOPE</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-mid\",\"messages\":[{\"role\":\"user\",\"content\":\"Say OK.\"}]}'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'valid token   HTTP %{http_code}\\n'</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$TOKEN</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-mid\",\"messages\":[{\"role\":\"user\",\"content\":\"Say OK.\"}]}'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">no token      HTTP 401</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">wrong scope   HTTP 401</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">valid token   HTTP 429</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Three curl calls printing no token HTTP 401, wrong scope HTTP 401, and valid token HTTP 429\" src=\"https://development-wec.wiline.com/docs/assets/images/h5-three-status-codes-0db858ce4b80dda45f7c2626ec09529c.png\" width=\"804\" height=\"293\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>The first two never reach a model. The second is the one that matters: a <strong>valid, correctly\nsigned, unexpired token from the right issuer</strong>, refused because it did not carry\n<code>gateway:invoke</code>. Identity alone is not authority.</p>\n<p>The third is the interesting one, and it is not a 200. It cleared authentication and scope, and\nwas then answered by a completely different part of the gateway — the budget and routing layer\nthe <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\">gateway series</a> configures, which on this box\nhad a cap already reached. That is the point of the shape rather than a flaw in it: a 401 means\n<em>we do not know who you are, or you may not do this</em>, and anything else means the request got\npast identity entirely and is now somebody else's decision. Authentication's job ends at the\nfirst two lines.</p>\n<p>Confirm the refusal came from your check rather than something incidental:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> logs llm-gateway </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--since</span><span class=\"token plain\"> 10m </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-iE</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'missing scope|401 Unauthorized'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-5</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">user_api_key_auth(): Exception occured - token is missing scope gateway:invoke</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Exception: token is missing scope gateway:invoke</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INFO:     \"POST /v1/chat/completions HTTP/1.1\" 401 Unauthorized</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The gateway log showing the exception token is missing scope gateway followed by a 401 Unauthorized response\" src=\"https://development-wec.wiline.com/docs/assets/images/h5-scope-refusal-log-aec61cc01ab970a987dee6c534e39c59.png\" width=\"939\" height=\"152\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Note where that reason lives. The caller receives a bare 401 with no explanation, which is\ncorrect — telling a client <em>\"your token is valid but lacks scope X\"</em> tells an attacker exactly\nwhat to go and get. It does mean that debugging this always means reading the gateway log, and\nnever the response body.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can give a headless agent its own identity: a confidential client restricted to one grant,\na scope that means something, and a gateway that verifies the signature locally and refuses\nanything without the scope — with the refusal proven from the log rather than assumed from a\nstatus code.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting--the-errors-this-run-actually-produced\">Troubleshooting — the errors this run actually produced<a href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/#troubleshooting--the-errors-this-run-actually-produced\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting — the errors this run actually produced\" title=\"Direct link to Troubleshooting — the errors this run actually produced\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"invalid_client\"><code>invalid_client</code><a href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/#invalid_client\" class=\"hash-link\" aria-label=\"Direct link to invalid_client\" title=\"Direct link to invalid_client\" translate=\"no\">​</a></h3>\n<p>The credentials did not authenticate at all. Before blaming authentik, check what actually\nlanded in your <code>.env</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">awk</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-F</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{printf \"%-18s %3d chars\\n\", $1, length($2)}'</span><span class=\"token plain\"> ~/agent-auth/.env</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AK_TOKEN_URL        48 chars</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AK_CLIENT_ID        40 chars</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AK_CLIENT_SECRET   128 chars</span><br></div></code></pre></div></div>\n<p>The client ID is 40 characters and the secret is 128. A zero-length value means the file is the\nproblem, not the provider.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"invalid_grant-with-everything-apparently-correct\"><code>invalid_grant</code>, with everything apparently correct<a href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/#invalid_grant-with-everything-apparently-correct\" class=\"hash-link\" aria-label=\"Direct link to invalid_grant-with-everything-apparently-correct\" title=\"Direct link to invalid_grant-with-everything-apparently-correct\" translate=\"no\">​</a></h3>\n<p>The provider is not paired to an application. See Step 2 — the Providers list flags it, the\nerror text does not.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"ak_token_url-unbound-variable-and-a-corrupted-secret\"><code>AK_TOKEN_URL: unbound variable</code>, and a corrupted secret<a href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/#ak_token_url-unbound-variable-and-a-corrupted-secret\" class=\"hash-link\" aria-label=\"Direct link to ak_token_url-unbound-variable-and-a-corrupted-secret\" title=\"Direct link to ak_token_url-unbound-variable-and-a-corrupted-secret\" translate=\"no\">​</a></h3>\n<p><code>echo 'X=1' &gt;&gt; .env</code> on a file whose last line has no trailing newline appends to that line\ninstead of creating a new one:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AK_CLIENT_SECRET=p94oDU...dgJcAK_TOKEN_URL=http://10.80.4.212:9100/application/o/token/</span><br></div></code></pre></div></div>\n<p>Two variables destroyed in one command, silently — the secret is now wrong <em>and</em> the new\nvariable does not exist. Check with <code>cat -A</code>, which marks line ends with <code>$</code>, and prefer\nwriting the whole file with a single heredoc over appending to it.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-config-change-had-no-effect\">The config change had no effect<a href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/#the-config-change-had-no-effect\" class=\"hash-link\" aria-label=\"Direct link to The config change had no effect\" title=\"Direct link to The config change had no effect\" translate=\"no\">​</a></h3>\n<p>The container was never restarted. See the warning in Step 4.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"401-with-no-reason-in-the-response\"><code>401</code> with no reason in the response<a href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/#401-with-no-reason-in-the-response\" class=\"hash-link\" aria-label=\"Direct link to 401-with-no-reason-in-the-response\" title=\"Direct link to 401-with-no-reason-in-the-response\" translate=\"no\">​</a></h3>\n<p>By design. The reason is in <code>docker logs llm-gateway</code>.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-this-did-and-didnt-buy-you\">What this did and didn't buy you<a href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/#what-this-did-and-didnt-buy-you\" class=\"hash-link\" aria-label=\"Direct link to What this did and didn't buy you\" title=\"Direct link to What this did and didn't buy you\" translate=\"no\">​</a></h2>\n<p>Done: a machine identity issued by your identity provider rather than minted by hand, a\ncredential that expires in five minutes, a scope that has to be requested and is checked on\nevery call, revocation that happens in one place, and a caller the gateway can name in its logs\nas something other than a key prefix.</p>\n<p>Not done:</p>\n<ul>\n<li class=\"\"><strong>The MCP server is still open.</strong> Everything here protects the gateway. The tools server from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/\">agent orchestration Part 4</a> still trusts anything that\ncan reach its port, <code>issue_refund</code> included. Same problem, one layer up, and the subject of\nthe next part.</li>\n<li class=\"\"><strong>One scope, one client.</strong> A real deployment has several agents with different scopes, and\n<code>scope_mappings</code> between scopes and models is where that goes.</li>\n<li class=\"\"><strong>The master key still works</strong>, deliberately, as the fallback in the handler. It remains the\nbreak-glass credential Part 4 described, and it still bypasses everything.</li>\n<li class=\"\"><strong>Still plain HTTP.</strong> The token crosses the network in the clear, and a token in the clear is\na key in the clear until it expires.</li>\n<li class=\"\"><strong>The service account is invisible.</strong> authentik created\n<code>ak-agent-gateway-client_credentials</code> without asking, and nothing in the token says it is a\nmachine rather than a person.</li>\n</ul>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>An identity for the agent, not a key</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>The MCP server. FastMCP ships a <code>JWTVerifier</code> that takes a <code>jwks_uri</code>, an <code>issuer</code>, an\n<code>audience</code> and <code>required_scopes</code> — the same four things configured here — so the tools server\ncan demand a scoped token before it will list a tool, let alone run one. That closes the gap\nagent orchestration Part 4 left open, where scoping happened in the client and the server\ntrusted whoever reached the port.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.goauthentik.io/docs/providers/oauth2/client_credentials\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">authentik — client credentials</a></li>\n<li class=\"\"><a href=\"https://docs.goauthentik.io/docs/add-secure-apps/providers/property-mappings/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">authentik — provider property mappings, and scope mappings with OAuth2</a></li>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/proxy/custom_auth\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">LiteLLM — custom auth</a></li>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/proxy/token_auth\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">LiteLLM — OIDC JWT-based auth</a>, the Enterprise path</li>\n<li class=\"\"><a href=\"https://pyjwt.readthedocs.io/en/stable/usage.html#retrieve-rsa-signing-keys-from-a-jwks-endpoint\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">PyJWT — <code>PyJWKClient</code></a></li>\n<li class=\"\"><a href=\"https://datatracker.ietf.org/doc/html/rfc6749#section-4.4\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">RFC 6749 §4.4 — the client credentials grant</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/",
            "title": "The key that expires: giving an agent its own identity at the gateway",
            "summary": "Part 4 ended with a shell, a static key and a model that answered. This replaces that key with a token your identity provider issues, that carries a scope, and that dies after five minutes. authentik issues it over client credentials, the gateway verifies it in twenty-five lines, and three status codes prove the boundary. Includes the requirement authentik doesn't document, and the licence wall you hit if you follow LiteLLM's own guide.",
            "date_modified": "2026-09-08T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "security",
                "oauth2",
                "oidc",
                "authentik",
                "identity",
                "litellm",
                "gateway",
                "agents",
                "hardening"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__mcp\" src=\"data:image/png;base64,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\" alt=\"Model Context Protocol\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"487\" height=\"249\" fill=\"none\" viewBox=\"0 0 487 249\" class=\"tutorialHero__brand\"><path fill=\"#1C3C3C\" fill-rule=\"evenodd\" d=\"M124.273.412h239.031c68.111 0 123.52 55.603 123.52 123.948s-55.409 123.948-123.52 123.948H124.273c-68.11 0-123.52-55.603-123.52-123.948S56.164.412 124.274.412M234.018 192.55c3.001 3.156 7.443 3 11.378 2.182l.039.02c1.827-1.486-.77-3.368-3.249-5.165-1.487-1.077-2.932-2.125-3.356-3.038 1.373-1.674-2.687-5.474-5.847-8.433-1.327-1.242-2.495-2.335-3.037-3.061-2.248-2.448-3.152-5.537-4.062-8.644-.603-2.061-1.208-4.131-2.211-6.027-6.176-14.339-13.248-28.561-23.165-40.699-6.373-8.067-13.646-15.291-20.922-22.518-4.691-4.66-9.382-9.319-13.835-14.206-4.581-4.73-7.338-10.556-10.1-16.392-2.312-4.886-4.627-9.779-8.018-14.04-10.268-15.196-42.686-19.346-47.44 2.124.019.662-.195 1.09-.78 1.52-2.63 1.928-4.968 4.11-6.935 6.76-4.812 6.721-5.553 18.118.448 24.158l.025-.388c.2-3.05.388-5.9 2.8-8.087 4.637 3.994 11.67 5.416 17.047 2.435 6.483 9.3 8.548 20.543 10.621 31.823 1.726 9.398 3.457 18.821 7.751 27.171l.266.443c2.524 4.202 5.089 8.472 8.326 12.142 1.176 1.823 3.59 3.791 6 5.755 3.18 2.591 6.353 5.177 6.663 7.416.015.974.01 1.961.006 2.954-.025 5.879-.051 11.967 3.716 16.801 2.084 4.228-3.02 8.475-7.131 7.949-2.254.312-4.716-.282-7.161-.871-3.346-.807-6.659-1.607-9.36-.064-.758.82-1.846.849-2.94.877-1.297.035-2.601.069-3.373 1.422-.158.402-.528.856-.913 1.328-.846 1.036-1.762 2.159-.665 3.016q.149-.111.294-.224c1.663-1.269 3.248-2.479 5.493-1.724-.299 1.658.772 2.103 1.843 2.547q.281.114.553.239c-.011.385-.087.774-.163 1.159-.18.921-.356 1.824.358 2.62.339-.344.639-.732.939-1.12.735-.95 1.474-1.904 2.802-2.25 2.92 3.9 5.862 2.28 9.554.248 4.164-2.293 9.281-5.111 16.396-1.125-2.727-.136-5.163.195-6.994 2.455-.448.507-.838 1.091-.039 1.754 4.21-2.728 5.961-1.748 7.61-.825 1.19.666 2.326 1.302 4.294.493.465-.242.93-.493 1.396-.745 3.161-1.705 6.366-3.434 10.118-2.839-2.803.808-3.8 2.584-4.888 4.524-.539.959-1.099 1.957-1.911 2.898-.429.429-.624.936-.137 1.656 5.869-.488 8.087-1.976 11.083-3.986 1.429-.959 3.036-2.038 5.302-3.183 2.505-1.542 5.009-.556 7.436.4 2.633 1.037 5.175 2.037 7.527-.264.743-.7 1.674-.708 2.602-.717a10 10 0 0 0 1.002-.043c-.732-3.92-4.861-3.874-9.052-3.827-4.847.054-9.778.109-9.632-5.972 4.504-3.076 4.546-8.415 4.585-13.461.01-1.218.019-2.419.091-3.567 3.313 1.847 6.817 3.291 10.299 4.725 3.276 1.35 6.533 2.692 9.593 4.354 3.195 5.143 8.182 11.962 14.826 11.514.175-.526.331-.974.526-1.5.383.067.787.169 1.199.273 1.743.442 3.611.915 4.509-1.15m130.213-58.45a20.54 20.54 0 0 0 14.504 5.994c5.44 0 10.658-2.156 14.505-5.994a20.44 20.44 0 0 0 6.007-14.469 20.44 20.44 0 0 0-6.007-14.469 20.54 20.54 0 0 0-21.882-4.624l-11.757-17.162-8.194 5.614 11.818 17.25a20.43 20.43 0 0 0-5.002 13.391 20.43 20.43 0 0 0 6.008 14.469m-36.808-55.576a20.55 20.55 0 0 0 21.408-1.964 20.46 20.46 0 0 0 7.34-10.483 20.4 20.4 0 0 0-.331-12.784 20.47 20.47 0 0 0-7.873-10.09 20.557 20.557 0 0 0-18.35-2.282 20.5 20.5 0 0 0-7.964 5.175 20.44 20.44 0 0 0-4.77 8.201 20.429 20.429 0 0 0 3.232 18.164 20.5 20.5 0 0 0 7.308 6.063m0 118.824a20.55 20.55 0 0 0 21.408-1.964 20.46 20.46 0 0 0 7.34-10.483 20.4 20.4 0 0 0-.331-12.783 20.47 20.47 0 0 0-7.873-10.092 20.55 20.55 0 0 0-26.314 2.894 20.45 20.45 0 0 0-4.77 8.201 20.43 20.43 0 0 0 3.232 18.164 20.5 20.5 0 0 0 7.308 6.063m18.857-72.629v-10.174h-31.394a20.3 20.3 0 0 0-4.398-8.342L322.3 88.704l-8.59-5.698-11.812 17.499a20.5 20.5 0 0 0-6.749-1.221 20.5 20.5 0 0 0-14.462 5.959 20.3 20.3 0 0 0-5.99 14.388 20.3 20.3 0 0 0 5.99 14.387 20.5 20.5 0 0 0 14.462 5.96 20.5 20.5 0 0 0 6.749-1.221l11.812 17.498 8.487-5.697-11.709-17.498a20.3 20.3 0 0 0 4.398-8.342z\" clip-rule=\"evenodd\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 4 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->4</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->4<!-- --> earned</span></div><div class=\"skillTracker__series\">Agent orchestration with LangGraph</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">State that survives a restart</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Hand work between agents</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Governed tools an agent can call</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">🏆</span><span class=\"skillTracker__skill\" data-state=\"current\">Engineer the context, not the prompt</span></span></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/\">Part 3</a> gave an agent five tools over MCP —\na calendar, a customer list, invoices, and a refund — and put a human in front of the refund.\nIt ended by admitting the obvious: <strong>one agent held all five.</strong> Nothing stopped the model\nreaching for <code>issue_refund</code> when it had been asked to book an appointment. The human pause was\nthe only thing in the way, and a pause only fires if the model calls the tool at all.</p>\n<p>This part removes the tool instead. The scheduler gets three tools and the billing agent gets\nthree, sharing one. Ask the scheduler to refund an invoice and it cannot — not because it was\ntold not to, not because a policy blocked it, but because <code>issue_refund</code> was never in the list\nit was given.</p>\n<p>Then <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/\">Part 2's supervisor</a> goes back on top,\nand the interesting property falls out: <strong>routing decides who works, scoping decides what is\npossible.</strong> A misrouted refund still cannot refund.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>The same box, server and database as <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/\">Part 3</a>:\none WEC Instance, Ubuntu 22.04, Python 3.10, <code>langchain</code> 1.4.0, <code>langgraph</code> 1.2.11, <code>mcp</code> 2.1.1,\n<code>fastmcp</code> 4.0.2. <code>office_tools.py</code> unchanged and still serving five tools on\n<code>127.0.0.1:8770</code>. Models through the LiteLLM gateway to WEC Inference. Reset the database with\n<code>./.venv/bin/python seed.py</code> before following along, so invoice 2 is open again.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-scoping-actually-means-here\">What \"scoping\" actually means here<a href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/#what-scoping-actually-means-here\" class=\"hash-link\" aria-label=\"Direct link to What &quot;scoping&quot; actually means here\" title=\"Direct link to What &quot;scoping&quot; actually means here\" translate=\"no\">​</a></h2>\n<p>There is no new mechanism in this post. <code>create_agent</code> takes a list of tools; we hand it a\nshorter list. That is the entire technique, and it is worth being precise about why it is\nstronger than the alternatives:</p>\n<ul>\n<li class=\"\"><strong>A system prompt</strong> telling the model not to issue refunds is a request. It survives exactly\nas long as the model cooperates.</li>\n<li class=\"\"><strong>A policy check inside the tool</strong> is real, but it runs <em>after</em> the model decided to call it,\nand it has to be written into every tool you ever add.</li>\n<li class=\"\"><strong>Not passing the tool</strong> removes the option from the model's context. There is nothing to\nrefuse, because there is nothing to call.</li>\n</ul>\n<p>The last one is the only one that doesn't depend on inference going well.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"one-server-two-toolboxes\">One server, two toolboxes<a href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/#one-server-two-toolboxes\" class=\"hash-link\" aria-label=\"Direct link to One server, two toolboxes\" title=\"Direct link to One server, two toolboxes\" translate=\"no\">​</a></h2>\n<!-- -->\n<p>The server offers all five to anyone who asks. What differs is which ones each agent is\n<strong>handed</strong>. <code>find_customer</code> goes to both, because looking a customer up is harmless.</p>\n<p>The blocked arrow is the whole post. The scheduler cannot call <code>issue_refund</code> — not because a\nrule forbids it, but because the tool is not in the list it was given, so it never reaches the\nmodel's context at all.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">The MCP server, virtualenv and seeded database from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/\">Part 3</a></li>\n<li class=\"\">The gateway key in <code>~/mcp-tools/.env</code>, as Part 3 set up</li>\n<li class=\"\">Python 3.10+</li>\n</ul>\n<p>Part 3 ran the server in the foreground, which cost a terminal and died with the SSH session.\nStart it detached instead — one session is enough for everything below:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">nohup</span><span class=\"token plain\"> ./.venv/bin/python office_tools.py </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> server.log </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> ss </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-tlnp</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8770</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--two-toolsets-from-one-server\">Step 1 — Two toolsets from one server<a href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/#step-1--two-toolsets-from-one-server\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Two toolsets from one server\" title=\"Direct link to Step 1 — Two toolsets from one server\" translate=\"no\">​</a></h2>\n<p>The server still offers five tools. The roles name subsets of them:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/scoped.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> sys</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">agents </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> create_agent</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">mcp </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> MCPAdapter</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain_openai </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> ChatOpenAI</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">checkpoint</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">memory </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> InMemorySaver</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MCP_URL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:8770/mcp\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ROLES </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"find_customer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"list_availability\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"book_slot\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">   </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"find_customer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"open_invoices\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"issue_refund\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">model </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ChatOpenAI</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    base_url</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:4000/v1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"GATEWAY_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"qwen-mid\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    temperature</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    role</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> request </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> MCPAdapter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">MCP_URL</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        catalog </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">t</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> t </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> t </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">list_tools</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"server offers : </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation builtin\">sorted</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">catalog</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        allowed </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">catalog</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">n</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> n </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> ROLES</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">role</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">role</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> can see : </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation\">t</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">name </span><span class=\"token string-interpolation interpolation keyword\" style=\"color:#00009f\">for</span><span class=\"token string-interpolation interpolation\"> t </span><span class=\"token string-interpolation interpolation keyword\" style=\"color:#00009f\">in</span><span class=\"token string-interpolation interpolation\"> allowed</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\\n\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        agent </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> create_agent</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">model</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> allowed</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> checkpointer</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">InMemorySaver</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        cfg </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"configurable\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"thread_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">role</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">-1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        out </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> agent</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">ainvoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"user\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> request</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> cfg</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"__interrupt__\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            req </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"__interrupt__\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">value</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"requests\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"PAUSED — the server is asking a human:\\n  </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">req</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'message'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"--- answer ---\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">out</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>The <code>checkpointer</code> is carried over from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/\">Part 3</a>: <code>issue_refund</code> pauses the run with an\ninterrupt, and an interrupted run needs somewhere to wait. The scheduler never triggers one, but\nboth roles run the same code.</p>\n<p>The whole of the technique is one line:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">allowed </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">catalog</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">n</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> n </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> ROLES</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">role</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<p><code>find_customer</code> appears in both roles — looking a customer up is harmless and both jobs need\nit. <code>book_slot</code> and <code>issue_refund</code> appear in exactly one each.</p>\n<p>The script prints both lists on every run for a reason. <code>server offers</code> is the full catalog the\nMCP server returned; <code>can see</code> is what we passed to <code>create_agent</code>. Keeping them side by side\nmakes it obvious that <strong>the server hid nothing</strong> — the narrowing happened in the client, on the\nline above.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--the-same-sentence-to-both-specialists\">Step 2 — The same sentence, to both specialists<a href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/#step-2--the-same-sentence-to-both-specialists\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — The same sentence, to both specialists\" title=\"Direct link to Step 2 — The same sentence, to both specialists\" translate=\"no\">​</a></h2>\n<p>Ask the billing agent to do its job:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-a</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python scoped.py billing </span><span class=\"token string\" style=\"color:#e3116c\">\"Refund Maria's open invoice.\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The billing agent holding find_customer, open_invoices and issue_refund, pausing with the server&amp;#39;s confirmation question about invoice 2\" src=\"https://development-wec.wiline.com/docs/assets/images/lg4-billing-paused-6de8f7f7d8c481db7073918444d88e53.png\" width=\"798\" height=\"204\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>It found Maria, resolved \"open invoice\" to invoice 2, called <code>issue_refund</code>, and stopped —\nbecause the tool demands a human. That is Part 3's guard, working.</p>\n<p>Now the identical sentence to the wrong specialist:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python scoped.py scheduler </span><span class=\"token string\" style=\"color:#e3116c\">\"Refund Maria's open invoice.\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The scheduler agent holding find_customer, list_availability and book_slot, replying that it has no tools for refunds or invoices\" src=\"https://development-wec.wiline.com/docs/assets/images/lg4-scheduler-refused-080822f69c551c7655e8e5fa5093178b.png\" width=\"800\" height=\"319\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<blockquote>\n<p>I don't have access to tools that can process refunds or handle invoices. The available tools\nI have are for finding customers, listing appointment availability, and booking appointment\nslots.</p>\n</blockquote>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>The politeness is not the guarantee</div><div class=\"admonitionContent_BuS1\"><p>That answer is accurate and well-mannered, and it would be a mistake to read it as the model\nbehaving well. The model <strong>could not</strong> have called <code>issue_refund</code>. It was never in the list, so\nit was never in the context, so there was no call to decline.</p><p>The distinction matters because a model that <em>does</em> hold a dangerous tool can also produce a\npolite sentence — including one describing an action it never took. We documented exactly that\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/news/langchain-mcp-first-class/\">in the news piece</a>: an era error handed to the model, and the\nmodel narrating an approval nobody would ever be asked for. A refusal you can trust is one the\nmodel had no way around.</p></div></div>\n<p>And the scheduler doing the job it <em>does</em> have tools for, so the restriction is targeted rather\nthan crippling:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python scoped.py scheduler </span><span class=\"token string\" style=\"color:#e3116c\">\"Book Maria into a free slot on 2026-09-05\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The scheduler agent booking Maria into the 09:00 slot on 2026-09-05\" src=\"https://development-wec.wiline.com/docs/assets/images/lg4-scheduler-books-56592bddc8b3d38f6f2f0f8d8aaf64ec.png\" width=\"801\" height=\"226\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--check-that-nothing-moved\">Step 3 — Check that nothing moved<a href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/#step-3--check-that-nothing-moved\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — Check that nothing moved\" title=\"Direct link to Step 3 — Check that nothing moved\" translate=\"no\">​</a></h2>\n<p>A refusal is a sentence. The database is the evidence.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">import sqlite3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for r in sqlite3.connect('office.db').execute('select id, customer_id, amount, status from invoices'): print(r)\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The invoices table showing invoice 2 still open after both the refusal and the paused run\" src=\"https://development-wec.wiline.com/docs/assets/images/lg4-nothing-happened-3c326c0194b72bcc27c1a87d9a2d05f7.png\" width=\"790\" height=\"145\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Invoice 2 is still <code>open</code>, for two different reasons: the scheduler had no tool to change it,\nand billing was stopped by the human gate before it could. Neither depended on the model\nchoosing well.</p>\n<p>It is worth seeing the same agent report a boring truth, too. Before we reset the database,\ninvoice 2 had already been refunded in Part 3 — and asked to refund it again, billing looked,\nfound nothing open, and said so:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The billing agent reporting that Maria has no open invoices, her invoices being either paid or already refunded\" src=\"https://development-wec.wiline.com/docs/assets/images/lg4-billing-nothing-open-b4f526ef9e6865039b0aefe2909f7918.png\" width=\"799\" height=\"207\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>No invented invoice, no cheerful confirmation. That is the behaviour that makes its other\nanswers worth anything, and it is not something to take on trust — it is something to check,\nwhich is why every claim in this post has a query behind it.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can give two agents different slices of the same MCP server, so that a capability one of\nthem must never have is absent from its context rather than forbidden by instruction — and\nverify in the data that the absence held.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--the-supervisor-back-on-top\">Step 4 — The supervisor, back on top<a href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/#step-4--the-supervisor-back-on-top\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — The supervisor, back on top\" title=\"Direct link to Step 4 — The supervisor, back on top\" translate=\"no\">​</a></h2>\n<p>Part 2 routed work between a scheduler and a billing agent that had no tools. Now they do.</p>\n<p><code>supervisor.py</code> is <code>scoped.py</code> with Part 2's graph around it: the same <code>ROLES</code>, <code>MCPAdapter</code> and\n<code>ChatOpenAI</code> setup, plus a <code>State</code> TypedDict, a <code>StateGraph</code>, and one agent built per role\ninside the <code>async with</code> block — <code>agents = {role: create_agent(model, [catalog[n] for n in names]) for role, names in ROLES.items()}</code>. Only the parts that are new to this post are shown here.</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/supervisor.py (the parts that matter)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">BILLING_WORDS </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"invoice\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"refund\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"bill\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"charge\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"payment\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">supervisor</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">Literal</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    nxt </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">any</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">w </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"request\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">lower</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> w </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> BILLING_WORDS</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"supervisor routes to </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nxt</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">goto</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">nxt</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> update</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"supervisor-&gt;</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nxt</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">role</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    out </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> agents</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">role</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">ainvoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"user\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"request\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"__interrupt__\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        q </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"__interrupt__\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">value</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"requests\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"message\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"answer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"PAUSED — </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">q</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">role</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"answer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">role</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">scheduler_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">billing_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>The <code>Command[Literal[\"scheduler\", \"billing\"]]</code> annotation is doing the same work it did in Part\n2 — it is the only declaration of where the supervisor may send work, and dropping it makes\nevery graph diagram lie.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-a</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python supervisor.py </span><span class=\"token string\" style=\"color:#e3116c\">\"Refund Maria's open invoice.\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The supervisor routing a refund request to the billing agent, which pauses on the human gate\" src=\"https://development-wec.wiline.com/docs/assets/images/lg4-supervisor-refund-eff231a6a04d02fc5430fa25025750e6.png\" width=\"801\" height=\"226\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python supervisor.py </span><span class=\"token string\" style=\"color:#e3116c\">\"Book Maria into a free slot on 2026-09-04\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The supervisor routing a booking request to the scheduler agent, which books the slot\" src=\"https://development-wec.wiline.com/docs/assets/images/lg4-supervisor-book-cb9829d31562e18281b2165e7fb3bf6e.png\" width=\"800\" height=\"244\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Routing is not the guarantee</div><div class=\"admonitionContent_BuS1\"><p>That supervisor is a keyword match. It is trivially foolable — \"settle Maria's outstanding\namount\" contains none of <code>BILLING_WORDS</code> and lands on the scheduler.</p><p>Which is the point worth taking away. <strong>A misrouted refund still cannot refund.</strong> The router\ndecides who gets the work; the toolsets decide what that agent can do with it. If routing were\nthe only control, every bug in the supervisor would be a security bug. With scoping underneath,\na routing mistake is just a bad answer.</p></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can put a router in front of scoped specialists and reason clearly about which failures are\ndangerous — because the blast radius of a routing bug is bounded by what the receiving agent\nwas given, not by what it was asked to do.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting--the-errors-this-run-actually-produced\">Troubleshooting — the errors this run actually produced<a href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/#troubleshooting--the-errors-this-run-actually-produced\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting — the errors this run actually produced\" title=\"Direct link to Troubleshooting — the errors this run actually produced\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"invalidupdateerror-expected-dict-got-coroutine-object\"><code>InvalidUpdateError: Expected dict, got &lt;coroutine object&gt;</code><a href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/#invalidupdateerror-expected-dict-got-coroutine-object\" class=\"hash-link\" aria-label=\"Direct link to invalidupdateerror-expected-dict-got-coroutine-object\" title=\"Direct link to invalidupdateerror-expected-dict-got-coroutine-object\" translate=\"no\">​</a></h3>\n<p>The node is an <code>async</code> function wrapped in a <code>lambda</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">lambda</span><span class=\"token plain\"> s</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> s</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># returns a coroutine</span><br></div></code></pre></div></div>\n<p>LangGraph calls it, gets a coroutine back instead of a state update, and raises. The real clue\nis the last line of the output — <code>RuntimeWarning: coroutine 'run' was never awaited</code> — which is\nthe part nobody reads. Define proper <code>async def</code> nodes instead.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"runtimeerror-client-failed-to-connect-all-connection-attempts-failed\"><code>RuntimeError: Client failed to connect: All connection attempts failed</code><a href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/#runtimeerror-client-failed-to-connect-all-connection-attempts-failed\" class=\"hash-link\" aria-label=\"Direct link to runtimeerror-client-failed-to-connect-all-connection-attempts-failed\" title=\"Direct link to runtimeerror-client-failed-to-connect-all-connection-attempts-failed\" translate=\"no\">​</a></h3>\n<p>Nothing is listening on 8770. If you started the MCP server in a foreground shell, it died with\nyour SSH session. The error names the <em>client</em>, which sends you looking in the wrong place.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ss </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-tlnp</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8770</span><br></div></code></pre></div></div>\n<p>Start it with <code>nohup ... &amp;</code> so it survives, and check the port before blaming the code.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"a-backgrounded-run-prints-nothing-for-minutes\">A backgrounded run prints nothing for minutes<a href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/#a-backgrounded-run-prints-nothing-for-minutes\" class=\"hash-link\" aria-label=\"Direct link to A backgrounded run prints nothing for minutes\" title=\"Direct link to A backgrounded run prints nothing for minutes\" translate=\"no\">​</a></h3>\n<p>Python buffers stdout when it is redirected to a file, so a long run looks identical to a hang.\nAdd <code>-u</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">nohup</span><span class=\"token plain\"> ./.venv/bin/python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-u</span><span class=\"token plain\"> supervisor.py </span><span class=\"token string\" style=\"color:#e3116c\">\"...\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> run.log </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-f</span><span class=\"token plain\"> run.log</span><br></div></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"keyerror-gateway_key\"><code>KeyError: 'GATEWAY_KEY'</code><a href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/#keyerror-gateway_key\" class=\"hash-link\" aria-label=\"Direct link to keyerror-gateway_key\" title=\"Direct link to keyerror-gateway_key\" translate=\"no\">​</a></h3>\n<p><code>source .env</code> sets shell variables, not environment variables, and <code>export</code> does not cross SSH\nsessions. Every command that starts the agent needs its own\n<code>set -a &amp;&amp; source .env &amp;&amp; set +a</code> — including the backgrounded ones.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-this-did-and-didnt-buy-you\">What this did and didn't buy you<a href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/#what-this-did-and-didnt-buy-you\" class=\"hash-link\" aria-label=\"Direct link to What this did and didn't buy you\" title=\"Direct link to What this did and didn't buy you\" translate=\"no\">​</a></h2>\n<p>Done: two agents drawing different subsets of one MCP server, a capability that is absent\nrather than forbidden, and a router whose mistakes cost correctness instead of money.</p>\n<p>Not done:</p>\n<ul>\n<li class=\"\"><strong>The MCP server still trusts everyone.</strong> Scoping happens in the <em>client</em>. Anything that can\nreach <code>127.0.0.1:8770</code> can call <code>issue_refund</code> directly, agent or not. This is a control on\nwhat your agents can do, not on what your server will accept.</li>\n<li class=\"\"><strong>No identity on the tool call.</strong> The server cannot tell the scheduler from the billing agent,\nbecause nothing in the protocol says which is calling.</li>\n<li class=\"\"><strong>The subsets are hardcoded.</strong> A real deployment reads them from the same place it reads team\nmembership — which is the <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/\">hardening series'</a>\nterritory, one layer down.</li>\n<li class=\"\"><strong>The supervisor is a keyword match</strong>, kept deliberately dumb so the scoping argument stands\non its own.</li>\n</ul>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Engineer the context, not the prompt</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>Push the boundary from the client to the server: <strong>authenticate the tool call</strong>, so the server\nknows which agent is asking and refuses <code>issue_refund</code> to anything that isn't billing. That is\nthe same question <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/\">Part 4 of the hardening series</a>\nasked about the gateway's own API — a control plane and a data plane, one layer apart — and\nFastMCP already supports bearer tokens and OAuth 2.1 for it.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.langchain.com/oss/python/langchain/mcp\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">MCP in LangChain</a> — connections, auth, elicitation</li>\n<li class=\"\"><a href=\"https://gofastmcp.com/clients/auth/oauth\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">FastMCP — client authentication</a></li>\n<li class=\"\"><a href=\"https://docs.langchain.com/oss/python/langgraph/graph-api\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">LangGraph — <code>Command</code> and conditional edges</a></li>\n<li class=\"\"><a href=\"https://modelcontextprotocol.io/specification/2026-07-28\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">MCP <code>2026-07-28</code> specification</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/",
            "title": "Split one MCP toolbox between two agents so the scheduler cannot issue refunds",
            "summary": "Part 3 handed one agent all five tools and let a human pause guard the dangerous one. That guard depends on the model asking. This splits the same MCP server between a scheduler and a billing agent, so the scheduler cannot refund an invoice for the simplest reason available — the tool was never in its list. Then the supervisor goes back on top, and routing turns out not to be the guarantee.",
            "date_modified": "2026-09-04T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "agents",
                "langgraph",
                "langchain",
                "mcp",
                "tools",
                "security",
                "self-hosting",
                "wec",
                "wec-inference"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__authentik\" src=\"data:image/png;base64,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\" alt=\"authentik\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">4</span><span class=\"skillTracker__skill\" data-state=\"current\">Membership, not just an account</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/\">Part 3</a> put authentik in front of Langfuse and\nended with an admission: the bindings step was left empty, so every account in authentik\ncould reach Langfuse and nothing would warn you.</p>\n<p>This part closes that gap on the <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\">LiteLLM gateway</a>\n— and the closing went wrong in a way worth more than the procedure. We configured the\nbindings in authentik's wizard, watched them appear in the wizard's own table, submitted,\nand ended up with <strong>zero bindings saved</strong>. The application was open to everyone, the UI said\nnothing, and the only reason we found out is that we tested with an account that should have\nbeen refused and wasn't.</p>\n<p>Then, once access control genuinely worked, we called the gateway's API from a shell with no\naccount, no session and no group. It answered normally. That one isn't a bug — it's the\ndifference between a <strong>control plane</strong> and a <strong>data plane</strong>, and assuming SSO covers both is\nhow people conclude they've secured something they haven't.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>One WEC Instance: 8 vCPU AMD EPYC 7601, 15 GB RAM, Docker 29.1.3, kernel 5.15.\nauthentik <code>2026.8.1</code>, the same instance from Part 3 on host port <code>9100</code>. LiteLLM\n<code>main-stable</code> from <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\">Part 1 of the gateway series</a>,\nbound to <code>127.0.0.1:4000</code>, models served over WEC Inference.</p><p>Plain HTTP on a private LAN, as in Part 3.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-new-here\">What's new here<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#whats-new-here\" class=\"hash-link\" aria-label=\"Direct link to What's new here\" title=\"Direct link to What's new here\" translate=\"no\">​</a></h2>\n<p>Part 3 taught the wizard — application, provider, redirect URI, scopes. That isn't repeated;\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/\">it's all in Part 3</a> and the steps are identical for\nany app. Three things are new:</p>\n<p><strong>A group</strong>, as the unit you grant access to, so that adding somebody to a team and giving\nthem access are the same action.</p>\n<p><strong>A binding</strong>, which attaches that group to an application, so reaching it requires\nmembership rather than merely existing.</p>\n<p><strong>A boundary</strong>, demonstrated rather than asserted: what SSO protects, and what it doesn't\ntouch at all.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">A working authentik, ideally the one from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/\">Part 3</a></li>\n<li class=\"\">A running LiteLLM gateway, from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\">Part 1 of the gateway series</a></li>\n<li class=\"\">Its <code>LITELLM_MASTER_KEY</code>, which you'll need to create a virtual key</li>\n<li class=\"\">SSH access to the box, because the gateway is bound to <code>127.0.0.1</code></li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"two-doors-one-of-them-guarded\">Two doors, one of them guarded<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#two-doors-one-of-them-guarded\" class=\"hash-link\" aria-label=\"Direct link to Two doors, one of them guarded\" title=\"Direct link to Two doors, one of them guarded\" translate=\"no\">​</a></h2>\n<!-- -->\n<p>Two boxes, no arrow between them — that's the point. Everything this post configures lives\nin the left one. The right one never consults authentik at all.</p>\n<p>There is also a third way in that the diagram deliberately leaves out, because it belongs to\nneither plane: LiteLLM's <strong>master key</strong> logs into the Admin UI directly, skipping the group\ncheck entirely. We hit it in Step 5.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--a-group-and-two-accounts\">Step 1 — A group, and two accounts<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#step-1--a-group-and-two-accounts\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — A group, and two accounts\" title=\"Direct link to Step 1 — A group, and two accounts\" translate=\"no\">​</a></h2>\n<p><strong>Directory → Groups → New Group</strong>, name it <code>gateway-admins</code>, and leave <strong>Superuser\nprivileges</strong> off. That flag makes members administrators <em>of authentik itself</em> and has\nnothing to do with which applications they can reach.</p>\n<p>authentik ships three groups already — <code>authentik Admins</code>, <code>authentik Agent-Users</code>,\n<code>authentik Read-only</code> — and <code>authentik Admins</code> already contains your <code>akadmin</code>. Binding that\none instead would work today and quietly mean \"anyone I ever make an authentik admin,\" which\nis not the same intent.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"authentik&amp;#39;s Groups list showing only the three built-in groups, with authentik Admins already holding one member\" src=\"https://development-wec.wiline.com/docs/assets/images/ak-groups-list-a17357dbbd4468da8f7232d5487ef77a.png\" width=\"2788\" height=\"1334\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Add <code>akadmin</code> to <code>gateway-admins</code>. Then <strong>Directory → Users → New User</strong>, type <strong>Internal</strong>,\nusername <code>contractor</code>, and <strong>no groups at all</strong>.</p>\n<p>Internal rather than External so that the only difference between the two accounts is\nmembership — any other difference muddies the result. Not a Service Account: those are\nmachine-to-machine and don't do interactive logins.</p>\n<p>authentik creates users without a password, and the Edit dialog has no password field — it's\na separate action on the user's detail page. From the shell:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.core.models import User</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">u = User.objects.get(username='contractor')</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">u.set_password('Contractor123!')</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">u.save()</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('password set for', u.username)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-2</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Silencing <code>ak shell</code></div><div class=\"admonitionContent_BuS1\"><p>Every <code>ak shell</code> call prints roughly sixty lines of bootstrap JSON before your output.\n<code>2&gt;/dev/null | tail -N</code> drops the noise and keeps the last N lines. Used throughout below.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--register-the-gateway-and-watch-the-slug\">Step 2 — Register the gateway, and watch the slug<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#step-2--register-the-gateway-and-watch-the-slug\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Register the gateway, and watch the slug\" title=\"Direct link to Step 2 — Register the gateway, and watch the slug\" translate=\"no\">​</a></h2>\n<p>Run the Part 3 wizard: application <strong>LiteLLM</strong>, provider <strong>OAuth2/OpenID</strong>, authorization\nflow <code>default-provider-authorization-explicit-consent</code>, client type <strong>Confidential</strong>.</p>\n<p>The gateway is published on <code>127.0.0.1:4000</code>, so your browser can't reach it — and an OIDC\nlogin happens in a browser. Open a tunnel from your workstation:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">ssh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-L</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">4000</span><span class=\"token plain\">:127.0.0.1:4000 ubuntu@10.80.4.212</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span><code>ssh -L</code> warns, then carries on without the tunnel</div><div class=\"admonitionContent_BuS1\"><p>If something already holds local port 4000, ssh prints <code>bind: Address already in use</code> and\nstill gives you a working shell — with a dead forward. The only symptom is\n<code>ERR_CONNECTION_REFUSED</code> in the browser, which reads like the gateway being down. Check the\nport before blaming the gateway: <code>lsof -nP -iTCP:4000 -sTCP:LISTEN</code>.</p></div></div>\n<p>Everything the browser types is now <code>localhost:4000</code>, so that's the redirect URI, mode\n<strong>Strict</strong>:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">http://localhost:4000/sso/callback</span><br></div></code></pre></div></div>\n<p>Now the first surprise. Ask authentik what it actually named things:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.core.models import Application</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for a in Application.objects.all():</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print(repr(a.name), '-&gt;', repr(a.slug))</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-5</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">'Langfuse' -&gt; 'langfuse'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">'LiteLLM' -&gt; 'lite-llm'</span><br></div></code></pre></div></div>\n<p><strong><code>lite-llm</code>, not <code>litellm</code>.</strong> The slugifier read the capitals in \"LiteLLM\" and split them.\nPart 3 called the slug load-bearing because it lands in the issuer URL, and here it is:\n<code>http://10.80.4.212:9100/application/o/lite-llm/</code>. Set the slug explicitly rather than\nletting a CamelCase name generate one, or carry that hyphen in every URL forever.</p>\n<p>Read the credentials back through the real slug:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.core.models import Application</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">p = Application.objects.get(slug='lite-llm').get_provider()</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('CLIENT_ID    :', p.client_id)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('CLIENT_SECRET:', p.client_secret)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-3</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--the-bindings-the-wizard-threw-away\">Step 3 — The bindings the wizard threw away<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#step-3--the-bindings-the-wizard-threw-away\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — The bindings the wizard threw away\" title=\"Direct link to Step 3 — The bindings the wizard threw away\" translate=\"no\">​</a></h2>\n<p>The wizard's step 4 is <strong>Configure Bindings</strong>, and Part 3 walked past it showing <code>No bound policies.</code> This time we didn't walk past it. We bound the group there, the wizard listed it\nin its table, and we submitted.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The wizard&amp;#39;s Configure Bindings step, empty, reading No bound policies\" src=\"https://development-wec.wiline.com/docs/assets/images/ak-bindings-wizard-empty-996a2adb20faf53dda84769592740e1c.png\" width=\"1800\" height=\"1033\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The same wizard step after binding, listing Group gateway-admins as enabled\" src=\"https://development-wec.wiline.com/docs/assets/images/ak-bindings-wizard-group-2c88ba715a495db175a3e85cf83df1f9.png\" width=\"1800\" height=\"1025\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Then we asked the database what existed:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.core.models import Application</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">app = Application.objects.get(slug='lite-llm')</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">bs = list(app.bindings.all())</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('binding count:', len(bs))</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for b in bs:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print('group', b.group, '| enabled', b.enabled, '| negate', b.negate)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-4</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">binding count: 0</span><br></div></code></pre></div></div>\n<p>Zero. The application's own bindings tab agreed — <strong>No Policies bound.</strong> Two independent\nsources, one conclusion: the wizard displayed access control that was never written.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The LiteLLM application&amp;#39;s own bindings tab reading No Policies bound\" src=\"https://development-wec.wiline.com/docs/assets/images/ak-bindings-none-71e30bfc7aa65339a119ee1ccb38d3ab.png\" width=\"1896\" height=\"901\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-danger admonition_xJq3 alert alert--danger\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z\"></path></svg></span>This is worse than skipping the step</div><div class=\"admonitionContent_BuS1\"><p>Part 3's lesson was \"don't skip bindings.\" The real hazard is that you can <em>complete</em> them,\nsee your rules in the wizard's table, submit, and end up with an application that admits\neveryone — no error, no warning, and a UI that looked correct the whole time.</p><p>An application with zero bindings isn't broken in any way authentik reports. It's doing\nexactly what it should: nothing bound means nobody is excluded. <strong>Verify bindings from the\ndatabase or the application's own bindings tab — never from the wizard.</strong></p></div></div>\n<p>Bind it from the application instead: <strong>Applications → LiteLLM → Policy / Group / User\nBindings → Create or bind…</strong>, then <strong>Bind a group</strong> under <em>Bind Existing…</em> → <code>gateway-admins</code>\n→ <strong>Create</strong>.</p>\n<p>Pick <strong>Bind a group</strong> specifically. The <em>Choose Policy Type</em> section below it creates\nbrand-new policies (Event Matcher, Expression, GeoIP); a group binding is already a rule and\nneeds none of them.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Create Binding dialog with gateway-admins selected, Enabled on, Negate Result off and Failure Result set to Don&amp;#39;t Pass\" src=\"https://development-wec.wiline.com/docs/assets/images/ak-bind-group-dialog-c20ad52e0f590a4df1c9a5e3aa411963.png\" width=\"1897\" height=\"945\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Then verify, because the table is what lied:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">binding count: 1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">group Group gateway-admins | enabled True | negate False</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can restrict an application to a group instead of leaving it open to every account —\nand, more importantly, verify the binding actually persisted (<code>app.bindings.all()</code>) rather\nthan trusting the wizard's own table.</p></div></div>\n<p>That page states the mode plainly: <em>\"The currently selected policy engine mode is ANY: Any\npolicy must match to grant access.\"</em> With one binding, ANY and ALL behave identically. With\ntwo they don't — <strong>ANY means one passing rule is enough</strong>, so a stray <code>User Contractor</code>\nbinding sitting beside the group would grant precisely the access this post is trying to\ndeny.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--point-the-gateway-at-authentik\">Step 4 — Point the gateway at authentik<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#step-4--point-the-gateway-at-authentik\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Point the gateway at authentik\" title=\"Direct link to Step 4 — Point the gateway at authentik\" translate=\"no\">​</a></h2>\n<p>LiteLLM reads generic OIDC settings from <code>GENERIC_*</code>. The endpoints come from the discovery\ndocument:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;&gt;</span><span class=\"token plain\"> ~/llm-gateway/.env </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">GENERIC_CLIENT_ID=&lt;client id from Step 2&gt;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">GENERIC_CLIENT_SECRET=&lt;client secret from Step 2&gt;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">GENERIC_AUTHORIZATION_ENDPOINT=http://10.80.4.212:9100/application/o/authorize/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">GENERIC_TOKEN_ENDPOINT=http://10.80.4.212:9100/application/o/token/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">GENERIC_USERINFO_ENDPOINT=http://10.80.4.212:9100/application/o/userinfo/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">PROXY_BASE_URL=http://localhost:4000</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div></code></pre></div></div>\n<p><code>PROXY_BASE_URL</code> is the one that bites. LiteLLM builds its redirect URI from it, so it has to\nequal both what the browser types and what authentik has registered. Ours disagreed on the\nfirst attempt and produced a <strong>Redirect URI Error</strong> — the <code>redirect_uri</code> in the address bar\nis what LiteLLM sent, the Strict value on the provider is what authentik expected, and a\nport, a hostname or a trailing slash between them is enough to fail.</p>\n<p>The gateway's compose file uses <code>env_file: .env</code>, so unlike Langfuse in Part 3 there's no\noverride file to write:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/llm-gateway </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> litellm </span><span class=\"token function\" style=\"color:#d73a49\">env</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> GENERIC_</span><br></div></code></pre></div></div>\n<p>Five. Before the edit it was zero — the same class of check Part 3 used to catch variables\nthat never reached the process.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--both-sides-of-the-binding\">Step 5 — Both sides of the binding<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#step-5--both-sides-of-the-binding\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — Both sides of the binding\" title=\"Direct link to Step 5 — Both sides of the binding\" translate=\"no\">​</a></h2>\n<p>Open <code>http://localhost:4000/ui</code> through the tunnel. The login page carries a finding this\npost didn't go looking for.</p>\n<p><strong>Login with SSO</strong> appears, as intended. Above it sits a username and password form, and\nLiteLLM's own panel explains it: <em>\"By default, Username is <code>admin</code> and Password is your set\nLiteLLM Proxy <code>MASTER_KEY</code>.\"</em></p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The LiteLLM login page showing a username and password form above the Login with SSO button, with a panel naming the master key as the default password\" src=\"https://development-wec.wiline.com/docs/assets/images/litellm-login-sso-a0616071a645fb27014bfa0b35203eaa.png\" width=\"1903\" height=\"997\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>The master key is a complete bypass. Anyone holding it reaches the admin UI without touching\nauthentik, without a group, without a binding. Everything configured in Steps 1–3 sits beside\na door that ignores all of it — and that door's key is also the gateway's root credential,\nable to mint API keys and reach every model.</p>\n<p><code>AUTO_REDIRECT_UI_LOGIN_TO_SSO=true</code> sends <code>/ui</code> straight to authentik instead of showing the\nform. It stops <em>advertising</em> the password path; it doesn't remove it. Treat the master key as\na break-glass credential and don't mistake the redirect for a fix.</p>\n<p>Sign in as <code>akadmin</code>, who is in <code>gateway-admins</code>. authentik shows the consent screen from\nPart 3's explicit-consent flow, and you land in the dashboard at <code>/ui/?login=success</code>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"authentik&amp;#39;s login screen reading Log in to continue to LiteLLM\" src=\"https://development-wec.wiline.com/docs/assets/images/ak-login-litellm-9e4c240eccffd4f7e739199f9b9c594a.png\" width=\"1800\" height=\"797\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The consent screen for akadmin, listing Email address and General Profile Information\" src=\"https://development-wec.wiline.com/docs/assets/images/ak-consent-akadmin-ea2ed59007a2b3e896f2d0437553763f.png\" width=\"1800\" height=\"774\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Now <code>contractor</code>.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>All incognito windows share one session</div><div class=\"admonitionContent_BuS1\"><p>Opening \"a new private window\" while another is already open reuses the same cookies, so\nauthentik signs you back in as the previous user. The symptom is an instant login that looks\nlike your access control failed. Close <strong>every</strong> incognito window, or use a different\nbrowser.</p></div></div>\n<p><strong>Permission denied. Request has been denied.</strong></p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"authentik refusing contractor with Permission denied and Request has been denied\" src=\"https://development-wec.wiline.com/docs/assets/images/ak-contractor-denied-14fb1a97964d42b993f3e49504ecb9dd.png\" width=\"1761\" height=\"694\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Note where that happened: authentik refused <em>before</em> the consent screen. The policy check\nruns ahead of consent, so a blocked user never sees the permissions page — which is also how\nwe caught the missing bindings earlier. When <code>contractor</code> reached a consent screen, that was\nthe tell.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-part-that-is-not-closed\">The part that is not closed<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#the-part-that-is-not-closed\" class=\"hash-link\" aria-label=\"Direct link to The part that is not closed\" title=\"Direct link to The part that is not closed\" translate=\"no\">​</a></h2>\n<p>From a shell on the box — no authentik account, no session, no group, nothing but a key:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer sk-...\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-mid\",\"messages\":[{\"role\":\"user\",\"content\":\"Say OK.\"}]}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'{model, answer: .choices[0].message.content}'</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"model\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"qwen-mid\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"answer\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\\n\\nOK\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The curl call answering normally from a shell with no authentik session\" src=\"https://development-wec.wiline.com/docs/assets/images/litellm-api-still-open-f3c81e08b6e2981f5cd844d8f236a4ec.png\" width=\"532\" height=\"186\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Nothing is broken. <strong>SSO never applied to that endpoint.</strong> Steps 1–5 protected <code>/ui</code> — the\ncontrol plane, where humans create keys and read spend. The data plane at <code>/v1/*</code>\nauthenticates machine callers with API keys, and it has to: an agent running at 3am can't\ncomplete a browser login with a consent screen.</p>\n<p>That key was itself created from a shell, with the master key, while no SSO session existed\nanywhere:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/llm-gateway </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/key/generate </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"key_alias\":\"sso-demo\",\"models\":[]}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.key'</span><br></div></code></pre></div></div>\n<p>The consequences, stated plainly, because \"we put SSO on the gateway\" gets said in meetings\nas though it settled the question:</p>\n<ul>\n<li class=\"\"><strong>Removing someone from <code>gateway-admins</code> does not revoke their keys.</strong> They lose the UI and\nkeep every key they made. Offboarding is two actions; only one is the one people remember.</li>\n<li class=\"\"><strong>A leaked key is unaffected by identity entirely.</strong> No session to expire, no group to\nremove it from.</li>\n<li class=\"\"><strong>Keys outlive people</strong>, unless you gave them an owner and an expiry when you made them.</li>\n</ul>\n<p>The gateway's own key model — scoped keys, budgets, expiry, per-key models — is what covers\nthat ground, and it's a different mechanism from the one configured here. Complementary, not\nsubstitutable.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"one-more-layer-getting-in-is-not-being-able-to-do-anything\">One more layer: getting in is not being able to do anything<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#one-more-layer-getting-in-is-not-being-able-to-do-anything\" class=\"hash-link\" aria-label=\"Direct link to One more layer: getting in is not being able to do anything\" title=\"Direct link to One more layer: getting in is not being able to do anything\" translate=\"no\">​</a></h2>\n<p>Signed in as <code>akadmin</code> through SSO, the Virtual Keys page had no <strong>Create Key</strong> button.</p>\n<p>LiteLLM assigns SSO users a default role that isn't admin. Passing authentik's binding got us\n<em>into</em> the UI; what we could <em>do</em> there is LiteLLM's own role model, which we never\nconfigured. Authentication, then authorization, then application-level roles — three distinct\nlayers, and clearing one says nothing about the next.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting--the-errors-this-run-actually-produced\">Troubleshooting — the errors this run actually produced<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#troubleshooting--the-errors-this-run-actually-produced\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting — the errors this run actually produced\" title=\"Direct link to Troubleshooting — the errors this run actually produced\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"application-matching-query-does-not-exist\"><code>Application matching query does not exist</code><a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#application-matching-query-does-not-exist\" class=\"hash-link\" aria-label=\"Direct link to application-matching-query-does-not-exist\" title=\"Direct link to application-matching-query-does-not-exist\" translate=\"no\">​</a></h3>\n<p>The <code>ak shell</code> lookup fails even though the application is visibly there in the UI. The slug\nisn't what you assumed — authentik derives it from the name, and a CamelCase name like\n<code>LiteLLM</code> becomes <code>lite-llm</code>. Ask rather than guess:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.core.models import Application</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for a in Application.objects.all():</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print(repr(a.name), '-&gt;', repr(a.slug))</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-5</span><br></div></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"redirect-uri-error\">Redirect URI Error<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#redirect-uri-error\" class=\"hash-link\" aria-label=\"Direct link to Redirect URI Error\" title=\"Direct link to Redirect URI Error\" translate=\"no\">​</a></h3>\n<p>authentik refuses before any login form. The <code>redirect_uri</code> in the address bar is what\nLiteLLM sent; the Strict value on the provider is what authentik expects. Ours disagreed\nbecause <code>PROXY_BASE_URL</code> said one host and the registered URI said another. Print what's\nregistered and compare character by character:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.core.models import Application</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">p = Application.objects.get(slug='lite-llm').get_provider()</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for r in p.redirect_uris:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print(repr(r.matching_mode), repr(r.url))</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-5</span><br></div></code></pre></div></div>\n<p>If you edit it and the error persists, <strong>Applications → Clear cache</strong> — authentik caches\nprovider config.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"err_connection_refused-on-localhost4000\"><code>ERR_CONNECTION_REFUSED</code> on <code>localhost:4000</code><a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#err_connection_refused-on-localhost4000\" class=\"hash-link\" aria-label=\"Direct link to err_connection_refused-on-localhost4000\" title=\"Direct link to err_connection_refused-on-localhost4000\" translate=\"no\">​</a></h3>\n<p>The tunnel isn't forwarding, even though ssh connected. If something already held the local\nport, ssh printed <code>bind: Address already in use</code> and carried on with a working shell and a\ndead forward. Check the port, then restart the tunnel:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">lsof</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-nP</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-iTCP:4000</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sTCP:LISTEN</span><br></div></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-user-i-expected-to-be-refused-got-in\">The user I expected to be refused got in<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#the-user-i-expected-to-be-refused-got-in\" class=\"hash-link\" aria-label=\"Direct link to The user I expected to be refused got in\" title=\"Direct link to The user I expected to be refused got in\" translate=\"no\">​</a></h3>\n<p>Two causes, both real here. Either a <strong>stray user binding</strong> grants them directly — policy\nengine mode ANY means one passing rule is enough — or <strong>the bindings were never saved</strong>. Ask\nthe database, not the UI:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.core.models import Application</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">app = Application.objects.get(slug='lite-llm')</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">bs = list(app.bindings.all())</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('binding count:', len(bs))</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for b in bs:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print('group', b.group, '| user', b.user, '| enabled', b.enabled, '| negate', b.negate)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-5</span><br></div></code></pre></div></div>\n<p>A user who reaches the <strong>consent screen</strong> has already passed the policy check — that's the\ntell. A blocked user never gets that far.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-refused-user-logs-straight-in-without-a-prompt\">The refused user logs straight in without a prompt<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#the-refused-user-logs-straight-in-without-a-prompt\" class=\"hash-link\" aria-label=\"Direct link to The refused user logs straight in without a prompt\" title=\"Direct link to The refused user logs straight in without a prompt\" translate=\"no\">​</a></h3>\n<p>Browser session, not policy. All Chrome incognito windows share one session, so opening\n\"a new private window\" while another is open keeps the previous user signed in. Close every\nincognito window, or use a different browser.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"no-create-key-button-in-the-admin-ui\">No <strong>Create Key</strong> button in the admin UI<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#no-create-key-button-in-the-admin-ui\" class=\"hash-link\" aria-label=\"Direct link to no-create-key-button-in-the-admin-ui\" title=\"Direct link to no-create-key-button-in-the-admin-ui\" translate=\"no\">​</a></h3>\n<p>You're signed in through SSO, and LiteLLM gives SSO users a default role that isn't admin.\nCreate keys from the shell with the master key, or configure LiteLLM's own roles — a\nseparate mechanism from anything authentik does.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can tell a control plane from a data plane on your own stack: put SSO and group\nmembership in front of an admin UI, then prove the API behind it still answers on a key\nalone — and name what that means for offboarding, leaked keys and keys that outlive people.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-this-did-and-didnt-buy-you\">What this did and didn't buy you<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#what-this-did-and-didnt-buy-you\" class=\"hash-link\" aria-label=\"Direct link to What this did and didn't buy you\" title=\"Direct link to What this did and didn't buy you\" translate=\"no\">​</a></h2>\n<p>Done: a group that means something, a binding that enforces it and that we verified in the\ndatabase rather than trusting a table, and an admin UI where access follows membership.</p>\n<p>Not done:</p>\n<ul>\n<li class=\"\"><strong>The API is untouched</strong>, by design, as demonstrated.</li>\n<li class=\"\"><strong>The master key still logs in</strong>, bypassing all of it.</li>\n<li class=\"\"><strong>SSO users have no LiteLLM role</strong> beyond the default.</li>\n<li class=\"\"><strong>Still plain HTTP.</strong> Every token and secret here crosses the network in the clear.</li>\n<li class=\"\"><strong>No MFA.</strong></li>\n</ul>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Membership, not just an account</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>TLS and real exposure. The gateway is on <code>127.0.0.1:4000</code> and reachable in this post only\nthrough an SSH tunnel; authentik is plain HTTP. Making either properly reachable means a\nreverse proxy, a hostname and a certificate — and every redirect URI, <code>PROXY_BASE_URL</code> and\nissuer changes the day that happens.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.goauthentik.io/docs/users-sources/groups/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">authentik docs — groups</a></li>\n<li class=\"\"><a href=\"https://docs.goauthentik.io/docs/customize/policies/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">authentik docs — bindings and the policy engine</a></li>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/proxy/ui\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">LiteLLM docs — SSO for the admin UI</a></li>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/proxy/virtual_keys\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">LiteLLM docs — virtual keys</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/",
            "title": "The binding that wasn't there: group access control, and what SSO doesn't protect",
            "summary": "Part 3 shipped SSO with empty bindings. Closing that gap on the LiteLLM gateway turned up something worse: authentik's wizard let us configure the bindings, showed them in its own table, and saved none of them — no error, no warning. Then, once access control actually worked, the API answered a request from a shell with no account, no session and no membership. Both of those are the post.",
            "date_modified": "2026-09-03T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "security",
                "sso",
                "oidc",
                "authentik",
                "identity",
                "rbac",
                "litellm",
                "gateway",
                "hardening"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__mcp\" src=\"data:image/png;base64,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\" alt=\"Model Context Protocol\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"487\" height=\"249\" fill=\"none\" viewBox=\"0 0 487 249\" class=\"tutorialHero__brand\"><path fill=\"#1C3C3C\" fill-rule=\"evenodd\" d=\"M124.273.412h239.031c68.111 0 123.52 55.603 123.52 123.948s-55.409 123.948-123.52 123.948H124.273c-68.11 0-123.52-55.603-123.52-123.948S56.164.412 124.274.412M234.018 192.55c3.001 3.156 7.443 3 11.378 2.182l.039.02c1.827-1.486-.77-3.368-3.249-5.165-1.487-1.077-2.932-2.125-3.356-3.038 1.373-1.674-2.687-5.474-5.847-8.433-1.327-1.242-2.495-2.335-3.037-3.061-2.248-2.448-3.152-5.537-4.062-8.644-.603-2.061-1.208-4.131-2.211-6.027-6.176-14.339-13.248-28.561-23.165-40.699-6.373-8.067-13.646-15.291-20.922-22.518-4.691-4.66-9.382-9.319-13.835-14.206-4.581-4.73-7.338-10.556-10.1-16.392-2.312-4.886-4.627-9.779-8.018-14.04-10.268-15.196-42.686-19.346-47.44 2.124.019.662-.195 1.09-.78 1.52-2.63 1.928-4.968 4.11-6.935 6.76-4.812 6.721-5.553 18.118.448 24.158l.025-.388c.2-3.05.388-5.9 2.8-8.087 4.637 3.994 11.67 5.416 17.047 2.435 6.483 9.3 8.548 20.543 10.621 31.823 1.726 9.398 3.457 18.821 7.751 27.171l.266.443c2.524 4.202 5.089 8.472 8.326 12.142 1.176 1.823 3.59 3.791 6 5.755 3.18 2.591 6.353 5.177 6.663 7.416.015.974.01 1.961.006 2.954-.025 5.879-.051 11.967 3.716 16.801 2.084 4.228-3.02 8.475-7.131 7.949-2.254.312-4.716-.282-7.161-.871-3.346-.807-6.659-1.607-9.36-.064-.758.82-1.846.849-2.94.877-1.297.035-2.601.069-3.373 1.422-.158.402-.528.856-.913 1.328-.846 1.036-1.762 2.159-.665 3.016q.149-.111.294-.224c1.663-1.269 3.248-2.479 5.493-1.724-.299 1.658.772 2.103 1.843 2.547q.281.114.553.239c-.011.385-.087.774-.163 1.159-.18.921-.356 1.824.358 2.62.339-.344.639-.732.939-1.12.735-.95 1.474-1.904 2.802-2.25 2.92 3.9 5.862 2.28 9.554.248 4.164-2.293 9.281-5.111 16.396-1.125-2.727-.136-5.163.195-6.994 2.455-.448.507-.838 1.091-.039 1.754 4.21-2.728 5.961-1.748 7.61-.825 1.19.666 2.326 1.302 4.294.493.465-.242.93-.493 1.396-.745 3.161-1.705 6.366-3.434 10.118-2.839-2.803.808-3.8 2.584-4.888 4.524-.539.959-1.099 1.957-1.911 2.898-.429.429-.624.936-.137 1.656 5.869-.488 8.087-1.976 11.083-3.986 1.429-.959 3.036-2.038 5.302-3.183 2.505-1.542 5.009-.556 7.436.4 2.633 1.037 5.175 2.037 7.527-.264.743-.7 1.674-.708 2.602-.717a10 10 0 0 0 1.002-.043c-.732-3.92-4.861-3.874-9.052-3.827-4.847.054-9.778.109-9.632-5.972 4.504-3.076 4.546-8.415 4.585-13.461.01-1.218.019-2.419.091-3.567 3.313 1.847 6.817 3.291 10.299 4.725 3.276 1.35 6.533 2.692 9.593 4.354 3.195 5.143 8.182 11.962 14.826 11.514.175-.526.331-.974.526-1.5.383.067.787.169 1.199.273 1.743.442 3.611.915 4.509-1.15m130.213-58.45a20.54 20.54 0 0 0 14.504 5.994c5.44 0 10.658-2.156 14.505-5.994a20.44 20.44 0 0 0 6.007-14.469 20.44 20.44 0 0 0-6.007-14.469 20.54 20.54 0 0 0-21.882-4.624l-11.757-17.162-8.194 5.614 11.818 17.25a20.43 20.43 0 0 0-5.002 13.391 20.43 20.43 0 0 0 6.008 14.469m-36.808-55.576a20.55 20.55 0 0 0 21.408-1.964 20.46 20.46 0 0 0 7.34-10.483 20.4 20.4 0 0 0-.331-12.784 20.47 20.47 0 0 0-7.873-10.09 20.557 20.557 0 0 0-18.35-2.282 20.5 20.5 0 0 0-7.964 5.175 20.44 20.44 0 0 0-4.77 8.201 20.429 20.429 0 0 0 3.232 18.164 20.5 20.5 0 0 0 7.308 6.063m0 118.824a20.55 20.55 0 0 0 21.408-1.964 20.46 20.46 0 0 0 7.34-10.483 20.4 20.4 0 0 0-.331-12.783 20.47 20.47 0 0 0-7.873-10.092 20.55 20.55 0 0 0-26.314 2.894 20.45 20.45 0 0 0-4.77 8.201 20.43 20.43 0 0 0 3.232 18.164 20.5 20.5 0 0 0 7.308 6.063m18.857-72.629v-10.174h-31.394a20.3 20.3 0 0 0-4.398-8.342L322.3 88.704l-8.59-5.698-11.812 17.499a20.5 20.5 0 0 0-6.749-1.221 20.5 20.5 0 0 0-14.462 5.959 20.3 20.3 0 0 0-5.99 14.388 20.3 20.3 0 0 0 5.99 14.387 20.5 20.5 0 0 0 14.462 5.96 20.5 20.5 0 0 0 6.749-1.221l11.812 17.498 8.487-5.697-11.709-17.498a20.3 20.3 0 0 0 4.398-8.342z\" clip-rule=\"evenodd\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 4 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->4</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->4<!-- --> earned</span></div><div class=\"skillTracker__series\">Agent orchestration with LangGraph</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">State that survives a restart</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Hand work between agents</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">3</span><span class=\"skillTracker__skill\" data-state=\"current\">Governed tools an agent can call</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Engineer the context, not the prompt</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/\">Part 1</a> gave an agent state that survives a\nrestart, and a pause where a human approves before it continues. <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/\">Part 2</a>\nput a supervisor in front of a <strong>scheduler</strong> and a <strong>billing</strong> agent and routed work between\nthem.</p>\n<p>Neither of those agents scheduled or billed anything. They discussed it. Every tool they had\nwas a function you wrote into the same process, and the interesting ones — look up a customer,\ntake a slot, move money — didn't exist.</p>\n<p>This part builds the toolbox those two roles need, over <strong>MCP</strong>: a calendar, a customer list,\ninvoices, and a refund. To keep the moving parts down we point <strong>one</strong> agent at all five tools\nrather than rebuilding Part 2's supervisor — the agent discovers them at runtime instead of\nbeing wired to them, chains them to answer a question, and writes rows you can go and check in\nthe database afterwards. Then the refund stops it dead and makes a human type the amount.</p>\n<p>Splitting these tools back across the scheduler and the billing agent — so the scheduler\n<em>cannot</em> see <code>issue_refund</code> at all — is the natural next step, and the last section says how.</p>\n<p>In July the MCP specification <a class=\"\" href=\"https://development-wec.wiline.com/docs/news/mcp-2026-07-28-spec/\">dropped sessions entirely</a>. On the\nday we ran this, LangChain shipped support for that revision in the main package. So this is a\nfirst run against a five-week-old protocol and a same-day client — which is why three things\nin here aren't in anyone's documentation yet.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>One WEC Instance: 8 vCPU AMD EPYC 7601, 15 GB RAM, Ubuntu 22.04, Python 3.10. <code>langchain</code>\n1.4.0, <code>langchain-openai</code> 1.6.0, <code>langgraph</code> 1.2.11, <code>mcp</code> 2.1.1, <code>fastmcp</code> 4.0.2 — all\ninstalled on the day of writing. Models served through the LiteLLM gateway from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\">Part 1 of the gateway series</a>, which forwards to\nWEC Inference. <code>langchain.mcp</code> is in beta and warns on every import; the API may move.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-mcp-actually-is\">What MCP actually is<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#what-mcp-actually-is\" class=\"hash-link\" aria-label=\"Direct link to What MCP actually is\" title=\"Direct link to What MCP actually is\" translate=\"no\">​</a></h2>\n<p>A server publishes tools. A client asks it what tools exist. The model picks one and the\nclient calls it. That's the whole protocol — the value is that \"asks what tools exist\" is\nstandardised, so any client can use any server without glue written for the pair.</p>\n<p>Three words you need:</p>\n<p><strong>Tool.</strong> A function the server exposes, with a description and a JSON schema for its\narguments. The description is what the model reads to decide whether to call it; the schema is\nwhat it must fill in. Nothing else about your code reaches the model.</p>\n<p><strong>Discovery.</strong> The client asks <code>tools/list</code> and gets that catalog back. Your agent learns what\nit can do at runtime instead of at the time you wrote it.</p>\n<p><strong>Elicitation.</strong> A tool that can't finish without asking a human something — confirming a\ndelete, supplying a parameter the model left out. Under the new spec this is an ordinary\nrequest the client retries with an answer attached.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-round-trip-that-replaced-the-session\">The round trip that replaced the session<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#the-round-trip-that-replaced-the-session\" class=\"hash-link\" aria-label=\"Direct link to The round trip that replaced the session\" title=\"Direct link to The round trip that replaced the session\" translate=\"no\">​</a></h2>\n<p>Under the old spec a tool that needed input held the connection open and asked down a\nback-channel. Under <code>2026-07-28</code> it returns, and the client calls it <strong>again</strong> with the answer\nattached. Two complete requests instead of one long-lived one:</p>\n<!-- -->\n<p>The tool runs <strong>twice</strong>. That's why its body starts with <code>if ctx.input_responses is None</code> — it\nhas to be written to be re-entered, not resumed. Nothing is held open in between, so the answer\ncan arrive after a redeploy, or at a different replica behind a load balancer.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">The virtualenv and WEC Inference access from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/\">Part 1</a></li>\n<li class=\"\">A <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\">LiteLLM gateway</a> with one virtual key, or any\nOpenAI-compatible endpoint you can point a model at</li>\n<li class=\"\">Python 3.10+</li>\n<li class=\"\">Two terminal sessions on the box — the server blocks one of them</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--install\">Step 1 — Install<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#step-1--install\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Install\" title=\"Direct link to Step 1 — Install\" translate=\"no\">​</a></h2>\n<p>MCP support now lives in the main <code>langchain</code> package. If you have <code>langchain-mcp-adapters</code>\ninstalled from before, it is the thing this replaces.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> venv .venv </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/pip </span><span class=\"token function\" style=\"color:#d73a49\">install</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-q</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"langchain[mcp]\"</span><span class=\"token plain\"> langchain-openai httpx </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/pip list </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-iE</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'langchain|langgraph|^mcp |fastmcp'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"pip list showing langchain 1.4.0, langgraph 1.2.11, mcp 2.1.1 and fastmcp 4.0.2\" src=\"https://development-wec.wiline.com/docs/assets/images/mcp-versions-5b3443d87f1bcddd968257037aceb26f.png\" width=\"749\" height=\"395\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><code>langchain[mcp]</code> needs <strong>1.4.0 or newer</strong> — below that, <code>MCPAdapter</code> doesn't exist.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span><code>mcp[cli]</code> is gone</div><div class=\"admonitionContent_BuS1\"><p>Older guides tell you to <code>pip install \"mcp[cli]\"</code>. On <code>mcp</code> 2.x that extra no longer exists,\nand pip fails with a wall of <code>does not provide the extra 'cli'</code> across sixty versions before\ngiving up with <code>ResolutionImpossible</code>. It reads like a dependency conflict; it's a removed\nextra.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--something-for-the-tools-to-act-on\">Step 2 — Something for the tools to act on<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#step-2--something-for-the-tools-to-act-on\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Something for the tools to act on\" title=\"Direct link to Step 2 — Something for the tools to act on\" translate=\"no\">​</a></h2>\n<p>The agents need data that exists. SQLite, seeded once — three customers, six slots (one\nalready taken), four invoices.</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/seed.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> sqlite3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">db </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sqlite3</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">connect</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"office.db\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">db</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">executescript</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">DROP TABLE IF EXISTS customers; DROP TABLE IF EXISTS slots; DROP TABLE IF EXISTS invoices;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">CREATE TABLE customers(id INTEGER PRIMARY KEY, name TEXT, email TEXT);</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">CREATE TABLE slots(id INTEGER PRIMARY KEY, day TEXT, time TEXT, customer_id INTEGER);</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">CREATE TABLE invoices(id INTEGER PRIMARY KEY, customer_id INTEGER, amount REAL, status TEXT);</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">INSERT INTO customers(name,email) VALUES</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\"> ('Maria Alvarez','maria@example.com'),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\"> ('Tomas Reis','tomas@example.com'),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\"> ('Priya Nair','priya@example.com');</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">INSERT INTO slots(day,time,customer_id) VALUES</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\"> ('2026-09-04','09:00',NULL),('2026-09-04','11:00',NULL),('2026-09-04','14:00',2),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\"> ('2026-09-05','09:00',NULL),('2026-09-05','11:00',NULL),('2026-09-05','16:00',NULL);</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">INSERT INTO invoices(customer_id,amount,status) VALUES</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\"> (1,240.00,'paid'),(1,80.00,'open'),(2,150.00,'paid'),(3,410.00,'open');</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">db</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">commit</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Two details are load-bearing. <strong>Maria has both a paid and an open invoice</strong>, so \"refund Maria's\ninvoice\" is ambiguous and the agent has to resolve it. <strong>Tomas already holds Friday at 14:00</strong>,\nso availability is a real question rather than \"everything\".</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--the-toolbox\">Step 3 — The toolbox<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#step-3--the-toolbox\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — The toolbox\" title=\"Direct link to Step 3 — The toolbox\" translate=\"no\">​</a></h2>\n<p>Five tools: three reads, one write, one that cannot finish without a human.</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/office_tools.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> sqlite3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastmcp </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> FastMCP</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> Context</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> mcp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">types </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> InputRequiredResult</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> ElicitRequest</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> ElicitRequestFormParams</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">DB </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">path</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">join</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">path</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">dirname</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">path</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">abspath</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">__file__</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"office.db\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">mcp </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> FastMCP</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"office-tools\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">q</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sql</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> args</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    con </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sqlite3</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">connect</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">DB</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> con</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">row_factory </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sqlite3</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">Row</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">try</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        rows </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> con</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">execute</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sql</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> args</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">fetchall</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> con</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">commit</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">r</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> r </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> rows</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">finally</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        con</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">close</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">w</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sql</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> args</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    con </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sqlite3</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">connect</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">DB</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">try</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        cur </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> con</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">execute</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sql</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> args</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> con</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">commit</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> cur</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">rowcount</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">finally</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        con</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">close</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@mcp</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">tool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">find_customer</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">query</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"Find customers whose name or email matches the query.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    like </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"%</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">query</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">%\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> q</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"SELECT id, name, email FROM customers WHERE name LIKE ? OR email LIKE ?\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">like</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> like</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@mcp</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">tool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">list_availability</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">day</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"List free appointment slots on a given day, formatted YYYY-MM-DD.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> q</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"SELECT id, day, time FROM slots WHERE day = ? AND customer_id IS NULL ORDER BY time\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">day</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@mcp</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">tool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">book_slot</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">slot_id</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">int</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> customer_id</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">int</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"Book a free slot for a customer. Fails if the slot is already taken.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    n </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> w</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"UPDATE slots SET customer_id = ? WHERE id = ? AND customer_id IS NULL\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">customer_id</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> slot_id</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> n </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Slot </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">slot_id</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> is already taken or does not exist.\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    row </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> q</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"SELECT day, time FROM slots WHERE id = ?\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">slot_id</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Booked slot </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">slot_id</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> (</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">row</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'day'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">row</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'time'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">) for customer </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">customer_id</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">.\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@mcp</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">tool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">open_invoices</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">customer_id</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">int</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"List a customer's invoices with their amounts and status.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> q</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"SELECT id, amount, status FROM invoices WHERE customer_id = ?\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">customer_id</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@mcp</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">tool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">issue_refund</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">invoice_id</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">int</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Context</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> InputRequiredResult</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"Refund an invoice. A human must approve the amount and give a reason.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    inv </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> q</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"SELECT id, customer_id, amount, status FROM invoices WHERE id = ?\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">invoice_id</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> inv</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"No invoice </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">invoice_id</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">.\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    inv </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> inv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    responses </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">input_responses</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> responses </span><span class=\"token keyword\" style=\"color:#00009f\">is</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        params </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ElicitRequestFormParams</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            message</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Refund invoice </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">inv</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'id'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> for customer </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">inv</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'customer_id'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">? \"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"It is </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">inv</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'status'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">, amount </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">inv</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'amount'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">.2f</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">. \"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:#e3116c\">\"Confirm the amount to refund and give a reason.\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            requested_schema</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"object\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:#e3116c\">\"properties\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                    </span><span class=\"token string\" style=\"color:#e3116c\">\"amount\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"number\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"description\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Amount to refund\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                    </span><span class=\"token string\" style=\"color:#e3116c\">\"reason\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"string\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"description\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Why this refund is being issued\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:#e3116c\">\"required\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"amount\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"reason\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> InputRequiredResult</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            result_type</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"input_required\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            input_requests</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"approve_refund\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> ElicitRequest</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">method</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"elicitation/create\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> params</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">params</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    answer </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> responses</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"approve_refund\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> answer</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">action </span><span class=\"token operator\" style=\"color:#393A34\">!=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"accept\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Refund declined - invoice </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">inv</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'id'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> unchanged.\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    w</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"UPDATE invoices SET status = 'refunded' WHERE id = ?\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">inv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Refunded </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">answer</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">content</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'amount'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">.2f</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> on invoice </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">inv</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'id'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">. Reason: </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">answer</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">content</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'reason'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> __name__ </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"__main__\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    mcp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">transport</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"http\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> host</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"127.0.0.1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> port</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">8770</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> stateless_http</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>The docstrings are not comments. They become the tool descriptions the model reads to choose,\nso write them as instructions to someone who can see nothing but that one line.</p>\n<p><code>book_slot</code> refuses a taken slot in SQL rather than checking first — <code>WHERE customer_id IS NULL</code>\nmeans two agents racing for the same slot can't both win.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python office_tools.py</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--the-session-that-shouldnt-exist\">Step 4 — The session that shouldn't exist<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#step-4--the-session-that-shouldnt-exist\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — The session that shouldn't exist\" title=\"Direct link to Step 4 — The session that shouldn't exist\" translate=\"no\">​</a></h2>\n<p>Leave it running. In a second session, ask what tools it has:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST http://127.0.0.1:8770/mcp </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Accept: application/json, text/event-stream'</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'</span><br></div></code></pre></div></div>\n<p>The first time we ran this, without <code>stateless_http=True</code>:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"jsonrpc\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token string\" style=\"color:#e3116c\">\"2.0\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token property\" style=\"color:#36acaa\">\"id\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token null keyword\" style=\"color:#00009f\">null</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token property\" style=\"color:#36acaa\">\"error\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"code\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">-32600</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token property\" style=\"color:#36acaa\">\"message\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token string\" style=\"color:#e3116c\">\"Bad Request: Missing session ID\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The MCP endpoint refusing a request with Bad Request: Missing session ID\" src=\"https://development-wec.wiline.com/docs/assets/images/mcp-session-error-7897aebee33998b591bd27b4606a6e6b.png\" width=\"666\" height=\"75\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-danger admonition_xJq3 alert alert--danger\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z\"></path></svg></span>The spec is stateless. Your server isn't, by default.</div><div class=\"admonitionContent_BuS1\"><p>The 2026-07-28 revision removed sessions from the protocol — no handshake, no session ID,\nnothing pinning a client to one instance. Every write-up of the change describes the client\nside accurately: there is nothing left to pin.</p><p><strong>FastMCP 4.0.2 still defaults to the stateful transport.</strong> You opt in with\n<code>stateless_http=True</code>, and until you do, the first request against a brand-new server fails\nciting a concept the spec deleted five weeks ago.</p><p><code>run_http_async</code> accepts both <code>stateless_http</code> and <code>stateless</code>; the docstring says the second\nis \"Alias for stateless_http for CLI consistency.\" One switch, two names, both defaulting to\n<code>None</code>.</p></div></div>\n<p>The startup line is where you check it:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"FastMCP starting without the stateless marker\" src=\"https://development-wec.wiline.com/docs/assets/images/mcp-server-start-9b984393ff73ca52793743fe31bbe9da.png\" width=\"756\" height=\"488\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"FastMCP starting with transport http (stateless)\" src=\"https://development-wec.wiline.com/docs/assets/images/mcp-server-stateless-8cd7f1d5201b5dc974f4726c52b23f9f.png\" width=\"776\" height=\"482\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>With the flag, the same request returns the catalog — no handshake, no session:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The five office tools listed with their descriptions\" src=\"https://development-wec.wiline.com/docs/assets/images/mcp-tools-list-c1f510e50217be4d48c633e07dafa45d.png\" width=\"744\" height=\"180\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><code>issue_refund</code>'s schema contains only <code>invoice_id</code> — the <code>ctx: Context</code> parameter is absent,\nbecause FastMCP injects it and the model can't set it. That's what makes a mandatory\nconfirmation mandatory.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--an-agent-that-discovers-its-tools\">Step 5 — An agent that discovers its tools<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#step-5--an-agent-that-discovers-its-tools\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — An agent that discovers its tools\" title=\"Direct link to Step 5 — An agent that discovers its tools\" translate=\"no\">​</a></h2>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/agent.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> sys</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">agents </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> create_agent</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">mcp </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> MCPAdapter</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain_openai </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> ChatOpenAI</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">model </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ChatOpenAI</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    base_url</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:4000/v1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"GATEWAY_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"qwen-mid\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    temperature</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> MCPAdapter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:8770/mcp\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        tools </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">list_tools</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"discovered:\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">t</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">name </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> t </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> tools</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        agent </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> create_agent</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">model</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> tools</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        out </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> agent</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">ainvoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"user\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"\\n--- answer ---\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">out</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Every published MCP example points the model at Anthropic, OpenAI or Gemini. This one points at\nyour own <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\">LiteLLM gateway</a>, which forwards to WEC\nInference — the tools are local, the model is yours, nothing leaves the box.</p>\n<p>Keep the key in a file rather than on the command line, where it would end up in your shell\nhistory and every screenshot:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">printf</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'GATEWAY_KEY=%s\\n'</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">chmod</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">600</span><span class=\"token plain\"> .env</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-a</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python agent.py </span><span class=\"token string\" style=\"color:#e3116c\">\"Who is Maria and what invoices does she have?\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The agent chaining find_customer into open_invoices and reporting both of Maria&amp;#39;s invoices\" src=\"https://development-wec.wiline.com/docs/assets/images/mcp-agent-lookup-d1e719855fca0eb978d89518c2a03cac.png\" width=\"983\" height=\"209\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>That answer took <strong>two tool calls in sequence</strong>: <code>find_customer(\"Maria\")</code> to get her id, then\n<code>open_invoices(1)</code> using it. Nothing routed that — the model read five descriptions and worked\nout the order.</p>\n<p>Now one that writes:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python agent.py </span><span class=\"token string\" style=\"color:#e3116c\">\"Book Maria into a free slot on 2026-09-04\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The agent finding Maria, listing Friday&amp;#39;s free slots, and booking 09:00\" src=\"https://development-wec.wiline.com/docs/assets/images/mcp-agent-booking-cf43e46e8fbeb7003efd7d2ceba1ddc4.png\" width=\"982\" height=\"157\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Three calls this time. And because a booking is a real change, check it somewhere the agent\ncan't reach:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/mcp-tools </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> ./.venv/bin/python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">import sqlite3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for r in sqlite3.connect('office.db').execute('select s.id,s.day,s.time,coalesce(c.name,</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">free</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">) from slots s left join customers c on c.id=s.customer_id order by s.day,s.time'): print(r)\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The slots table showing Maria booked into 2026-09-04 09:00\" src=\"https://development-wec.wiline.com/docs/assets/images/mcp-verify-db-e3eda545965f9b86a42918a3fd4bc674.png\" width=\"992\" height=\"173\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can stand up an MCP server over a real datastore, point an agent at it, and have it chain\ndiscovered tools into a change you can verify in the database — with the model on your own\ngateway rather than a vendor's.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-6--the-tool-that-refuses-to-finish-alone\">Step 6 — The tool that refuses to finish alone<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#step-6--the-tool-that-refuses-to-finish-alone\" class=\"hash-link\" aria-label=\"Direct link to Step 6 — The tool that refuses to finish alone\" title=\"Direct link to Step 6 — The tool that refuses to finish alone\" translate=\"no\">​</a></h2>\n<p>Part 1's approval pause was something <em>you</em> put in the graph. This one comes from the tool, and\nthe agent has no way to skip it.</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/elicit.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> os</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">agents </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> create_agent</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">mcp </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> MCPAdapter</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain_openai </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> ChatOpenAI</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">checkpoint</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">memory </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> InMemorySaver</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">types </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Command</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">model </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ChatOpenAI</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">base_url</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:4000/v1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                   api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"GATEWAY_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"qwen-mid\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> temperature</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">ask_human</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">req</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"\\n&gt;&gt;&gt; </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">req</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'message'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    props </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">req</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"requested_schema\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"properties\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    content </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> name</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> spec </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> props</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">items</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        raw </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">input</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"    </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">name</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> (</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">spec</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">get</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'type'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">,</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'string'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">): \"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">strip</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        content</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">float</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">raw</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> spec</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"number\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> raw</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"action\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"accept\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> content</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> MCPAdapter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:8770/mcp\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        tools </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">list_tools</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        agent </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> create_agent</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">model</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> tools</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> checkpointer</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">InMemorySaver</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        cfg </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"configurable\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"thread_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"refund-1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        out </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> agent</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">ainvoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"user\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Refund Maria's open invoice.\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> cfg</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"__interrupt__\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"NO INTERRUPT:\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        payload </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> out</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"__interrupt__\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">value</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"=== INTERRUPT ===\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">dumps</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">payload</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> indent</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        responses </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">r</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"key\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> ask_human</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">r</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> r </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> payload</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"requests\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"\\n=== RESUMING ===\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        out </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> agent</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">ainvoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">resume</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"responses\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> responses</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> cfg</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">out</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>The <code>checkpointer</code> and <code>thread_id</code> are not optional — an interrupted run needs somewhere to\nwait. Same requirement as Part 1's approval pause.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The interrupt payload, the typed amount and reason, and the resumed result\" src=\"https://development-wec.wiline.com/docs/assets/images/mcp-elicit-full-b00707c0653a77cd26dcf4221b184fa5.png\" width=\"1044\" height=\"732\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Read the order of events. The model resolved <em>\"Maria's open invoice\"</em> to invoice 2 by itself —\nshe has two, and it picked the open one. Then it stopped. <strong>The amount and the reason were\ntyped by a human</strong>, and only then did the refund happen.</p>\n<p>The payload is <code>{\"type\": \"mcp_elicitation\", \"tool_name\": \"issue_refund\", \"requests\": [...]}</code>.\n<code>type</code> is the discriminator, so a graph with several kinds of interrupt can tell which came\nfrom MCP. Each request carries a <code>key</code>, a <code>message</code> for a human, a <code>mode</code> of <code>form</code> or <code>url</code>,\nand for a form the <code>requested_schema</code> the answer must satisfy — here two required fields, one\nof them a number. You resume with one answer per key.</p>\n<p>And verify, again outside the agent:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The invoices table showing invoice 2 as refunded\" src=\"https://development-wec.wiline.com/docs/assets/images/mcp-verify-refund-3f9c62f71c37c5e0339cf4d1a4da57df.png\" width=\"1043\" height=\"356\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can make a tool refuse to finish without a human — returning a typed form the model cannot\nfill in itself, pausing the run as a LangGraph interrupt, and resuming it with an answer a\nperson actually typed.</p></div></div>\n<div class=\"theme-admonition theme-admonition-danger admonition_xJq3 alert alert--danger\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z\"></path></svg></span><code>ctx.elicit</code> is the old API, and its error lands in the model's context</div><div class=\"admonitionContent_BuS1\"><p>Every FastMCP elicitation example you will find calls <code>await ctx.elicit(message, response_type)</code>\ninside the tool. That is the <strong>handshake-era</strong> mechanism: it blocks mid-execution and speaks\nover the session's back-channel — the back-channel the stateless transport doesn't have.</p><p>Our first attempt used it. The agent didn't pause — but not because nothing was raised. FastMCP\nraised its era error exactly as documented, LangChain converted it into a <code>ToolMessage</code> with\n<code>status=\"error\"</code> (deliberately, \"instead of ending the run\"), and the model read that error and\nnarrated it as progress:</p><p><span class=\"zoomImage__wrap\"><img alt=\"The agent run printing interrupt raised? False, issue_refund with status=error carrying the era error, the model replying that it has initiated the refund and a human must approve, and sqlite3 showing invoice 2 still open\" src=\"https://development-wec.wiline.com/docs/assets/images/mcp-elicit-old-api-ec02d789aa6970b70a130f83067c235b.png\" width=\"1038\" height=\"437\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p><p>No interrupt, exit code 0, and the invoice still <code>open</code>. Nothing was refunded and nothing is\nwaiting for a human — but the user was told an approval is pending.</p><p>On the modern protocol a tool <strong>returns</strong> an <code>InputRequiredResult</code> describing what it needs, and\nexits. The client answers and <strong>retries the whole tool call</strong> with the answer attached. Each\nround is a complete request, which is why it survives load balancing and redeploys — and why\nthe tool body must be written to be re-entered rather than resumed. Ours does that with\n<code>if ctx.input_responses is None</code>.</p><p>FastMCP's docs are explicit that <code>ctx.elicit</code> works only on connections <code>≤ 2025-11-25</code>, and that\na multi-era server should branch on <code>ctx.request_context.protocol_version</code>. They also promise the\nmismatch \"raises a clear era error rather than failing obscurely\" — and it does. The error just\nlands in the model's context rather than yours.</p></div></div>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>The announcement's snippet doesn't run</div><div class=\"admonitionContent_BuS1\"><p>LangChain's post reads the interrupt as <code>paused[\"__interrupt__\"][0].value.requests[0]</code> —\nattribute access. <code>MCPElicitationInterrupt</code> is a <code>TypedDict</code> (see <code>langchain/mcp/elicitation.py</code>\nin 1.4.0), so at runtime it is a dict and <code>.requests</code> doesn't resolve. Use <code>value[\"requests\"]</code> —\nas the next line of their own snippet does, reading <code>question[\"key\"]</code>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-7--caching-and-why-it-does-nothing-here\">Step 7 — Caching, and why it does nothing here<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#step-7--caching-and-why-it-does-nothing-here\" class=\"hash-link\" aria-label=\"Direct link to Step 7 — Caching, and why it does nothing here\" title=\"Direct link to Step 7 — Caching, and why it does nothing here\" translate=\"no\">​</a></h2>\n<p>Every run starts by discovering tools, which is a round trip before the model sees anything.\nThe new spec makes that cacheable:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/mcp-tools/cached.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> time</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastmcp </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Client</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">mcp </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> MCPAdapter</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    client </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> Client</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:8770/mcp\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> cache</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> MCPAdapter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> i </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            t </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">perf_counter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            tools </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> adapter</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">list_tools</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">cache_mode</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"use\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"discovery </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">i</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">: </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation builtin\">len</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">tools</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> tools in </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">time</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">perf_counter</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation operator\" style=\"color:#393A34\">-</span><span class=\"token string-interpolation interpolation\">t</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation operator\" style=\"color:#393A34\">*</span><span class=\"token string-interpolation interpolation number\" style=\"color:#36acaa\">1000</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">.1f</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> ms\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">discovery 1: 5 tools in 14.6 ms</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">discovery 2: 5 tools in 12.5 ms</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Two discoveries of five tools timed at 14.6 ms and 12.5 ms, showing no cache effect\" src=\"https://development-wec.wiline.com/docs/assets/images/mcp-cache-bbadf88f6b26a069ec15e3b3db635c02.png\" width=\"801\" height=\"125\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>No effect — and that is correct behaviour, not a broken cache.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span><code>cache=True</code> is necessary, not sufficient</div><div class=\"admonitionContent_BuS1\"><p>The client cache \"respects the <code>ttlMs</code> and <code>cacheScope</code> hints the server attaches to each\nresponse\" and works only against modern-era servers that advertise them. Our server advertises\nnone, so both calls go to the network.</p><p>Note also that <code>cache=True</code> goes on the <strong><code>fastmcp.Client</code></strong>, not on <code>MCPAdapter</code> — the\nadapter takes exactly one argument. And the cache belongs to the client, so one client per\ncaller keeps catalogs from crossing between tenants.</p></div></div>\n<p>Over loopback there was nothing to save anyway: 12–15 ms is the round trip. Caching is for remote\nservers with real latency and a TTL to honour.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting--the-errors-this-run-actually-produced\">Troubleshooting — the errors this run actually produced<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#troubleshooting--the-errors-this-run-actually-produced\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting — the errors this run actually produced\" title=\"Direct link to Troubleshooting — the errors this run actually produced\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"resolutionimpossible-installing-mcpcli\"><code>ResolutionImpossible</code> installing <code>mcp[cli]</code><a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#resolutionimpossible-installing-mcpcli\" class=\"hash-link\" aria-label=\"Direct link to resolutionimpossible-installing-mcpcli\" title=\"Direct link to resolutionimpossible-installing-mcpcli\" translate=\"no\">​</a></h3>\n<p>The <code>cli</code> extra was removed in <code>mcp</code> 2.x. Install <code>\"langchain[mcp]\"</code> instead; it pulls a\ncompatible <code>mcp</code> and <code>fastmcp</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"bad-request-missing-session-id\"><code>Bad Request: Missing session ID</code><a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#bad-request-missing-session-id\" class=\"hash-link\" aria-label=\"Direct link to bad-request-missing-session-id\" title=\"Direct link to bad-request-missing-session-id\" translate=\"no\">​</a></h3>\n<p>Your server is running the stateful transport. Add <code>stateless_http=True</code> to <code>mcp.run(...)</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"keyerror-gateway_key\"><code>KeyError: 'GATEWAY_KEY'</code><a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#keyerror-gateway_key\" class=\"hash-link\" aria-label=\"Direct link to keyerror-gateway_key\" title=\"Direct link to keyerror-gateway_key\" translate=\"no\">​</a></h3>\n<p><code>source .env</code> sets shell variables, not environment variables — the Python child never sees\nthem. Use <code>set -a &amp;&amp; source .env &amp;&amp; set +a</code>. And <code>export</code> does not cross SSH sessions, so a\nsecond terminal needs its own <code>source</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"address-already-in-use-after-restarting-the-server\"><code>address already in use</code> after restarting the server<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#address-already-in-use-after-restarting-the-server\" class=\"hash-link\" aria-label=\"Direct link to address-already-in-use-after-restarting-the-server\" title=\"Direct link to address-already-in-use-after-restarting-the-server\" translate=\"no\">​</a></h3>\n<p>The previous instance is still holding the port — a crashed restart leaves the old process\nalive, so the fix you just made appears not to work. <code>pkill -f office_tools.py</code>, then check\nwith <code>ss -tlnp | grep 8770</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-agent-never-pauses-on-a-destructive-tool\">The agent never pauses on a destructive tool<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#the-agent-never-pauses-on-a-destructive-tool\" class=\"hash-link\" aria-label=\"Direct link to The agent never pauses on a destructive tool\" title=\"Direct link to The agent never pauses on a destructive tool\" translate=\"no\">​</a></h3>\n<p>The tool is calling <code>ctx.elicit</code> rather than returning <code>InputRequiredResult</code>. On a stateless\nconnection there is no back-channel. The era error is raised, but it arrives as a failed\n<code>ToolMessage</code> the model reads and talks past, so the run ends normally with nothing pending.\nCheck the tool messages, not just the final answer.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-this-did-and-didnt-buy-you\">What this did and didn't buy you<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#what-this-did-and-didnt-buy-you\" class=\"hash-link\" aria-label=\"Direct link to What this did and didn't buy you\" title=\"Direct link to What this did and didn't buy you\" translate=\"no\">​</a></h2>\n<p>Done: an MCP server over a real datastore, an agent that discovers tools rather than being\nwired to them and chains them without routing, two changes you verified in the database rather\nthan taking the model's word for, and a refund that cannot happen without a human supplying the\nnumber.</p>\n<p>Not done:</p>\n<ul>\n<li class=\"\"><strong>No auth on the MCP server.</strong> It is bound to <code>127.0.0.1</code> and anyone on the box can call every\ntool, including <code>issue_refund</code>. FastMCP supports bearer tokens and OAuth 2.1; we configured\nneither.</li>\n<li class=\"\"><strong>The tools trust the caller completely.</strong> <code>book_slot</code> will book for any <code>customer_id</code> it is\ngiven. There is no notion of <em>who</em> is asking — the same gap\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/\">Part 4 of the hardening series</a> found\nbetween a control plane and a data plane.</li>\n<li class=\"\"><strong>One server.</strong> <code>ClientGroup</code> connects several at once and prefixes tool names by server; we\nused a single target.</li>\n<li class=\"\"><strong>SQLite, single process.</strong> Fine for one agent; the row-level guard in <code>book_slot</code> is doing\nmore work than it looks under any real concurrency.</li>\n<li class=\"\"><strong><code>langchain.mcp</code> is beta</strong> and warns on every import.</li>\n</ul>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Governed tools an agent can call</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p><strong>Scope the tools per agent.</strong> Right now one agent holds all five, so nothing stops the model\nchoosing <code>issue_refund</code> when it was asked to book a slot — the human pause is the only guard.\nPart 2's supervisor already routes between a scheduler and a billing agent; hand each of them\nonly its own slice of <code>adapter.list_tools()</code> and the scheduler cannot issue a refund because it\nhas never been told the tool exists. That is a stronger guarantee than asking a model nicely.</p>\n<p><strong>Then authentication</strong>, so a tool call carries an identity rather than trusting whoever reached\nthe port — the same question <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/\">Part 4 of the hardening series</a>\nasked about the gateway's own API, one layer up.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.langchain.com/oss/python/langchain/mcp\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">MCP in LangChain</a> — quickstart, connections, auth, elicitation</li>\n<li class=\"\"><a href=\"https://docs.langchain.com/oss/python/migrate/langchain-mcp-adapters\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Migrating from <code>langchain-mcp-adapters</code></a></li>\n<li class=\"\"><a href=\"https://gofastmcp.com/clients/client\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">FastMCP client</a> — transports, auth, response caching</li>\n<li class=\"\"><a href=\"https://modelcontextprotocol.io/specification/2026-07-28\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">MCP <code>2026-07-28</code> specification</a></li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/news/mcp-2026-07-28-spec/\">Our write-up of the spec revision</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/",
            "title": "Book appointments and refund invoices from a LangGraph agent over MCP",
            "summary": "The scheduler and billing agents from Parts 1 and 2 never actually scheduled or billed anything. This gives them a calendar, a customer list and a refund that no model can issue alone — over MCP, on the stateless spec, with the model on WEC Inference. Includes three things nobody documents: your server still defaults to sessions, ctx.elicit is the old API and its era error goes to the model, not to you, and cache=True does nothing unless the server advertises a TTL.",
            "date_modified": "2026-09-03T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "agents",
                "langgraph",
                "langchain",
                "mcp",
                "tools",
                "self-hosting",
                "wec",
                "wec-inference"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__authentik\" src=\"data:image/png;base64,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\" alt=\"authentik\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2255\" height=\"527\" fill=\"none\" viewBox=\"0 0 2255 527\" class=\"tutorialHero__langfuse\"><path fill=\"#1B1917\" d=\"M652.669 116.433c0-10.261-7.683-17.956-17.926-17.956H607V60h87.923v316.366h-42.254zM804.056 379.786c-39.693 0-72.131-27.362-72.131-68.831 0-41.042 28.596-69.686 84.935-69.686h39.693c7.256 0 12.805-5.558 12.805-12.826v-8.55c0-25.651-20.487-38.905-40.547-38.905-18.353 0-33.291 8.123-41.828 26.934h-45.668c11.95-44.462 46.095-65.41 88.349-65.41 40.547 0 82.374 22.231 82.374 77.808v156.046h-42.68v-14.109c0-5.13-5.549-7.695-9.817-4.702-16.646 11.97-32.438 22.231-55.485 22.231m5.975-38.477c18.78 0 34.572-9.406 52.498-27.362 4.695-4.702 6.829-10.688 6.829-17.1v-6.413c0-7.268-5.549-12.826-12.805-12.826H815.58c-27.743 0-40.974 12.398-40.974 31.637 0 17.956 12.377 32.064 35.425 32.064M961.855 376.366V147.642h42.255v19.666c0 5.13 6.4 6.84 10.24 2.992 13.23-12.825 31.59-27.788 60.61-27.788 36.71 0 70.85 23.086 70.85 75.671v158.183h-42.25V227.161c0-29.499-17.93-45.745-39.7-45.745-20.91 0-35 11.971-49.93 29.499-7.26 8.978-9.82 19.238-9.82 30.354v135.097zM1287.48 467c-52.5 0-85.79-24.796-95.61-63.273h46.1c7.25 15.818 20.06 25.651 45.67 25.651 34.14 0 55.48-20.093 55.48-65.838v-7.268c0-5.13-4.27-7.695-9.39-3.42-14.08 12.398-32.01 20.093-49.08 20.093-58.05 0-96.46-44.889-96.46-115.003 0-70.113 44.39-115.43 98.17-115.43 15.79 0 30.3 4.275 44.38 14.963 5.98 4.275 12.38.855 12.38-5.986v-3.847h42.26V363.54c0 72.678-43.97 103.46-93.9 103.46m-2.56-132.959c19.2 0 33.29-8.55 44.81-20.949 7.26-8.122 9.39-13.68 9.39-26.506v-61.563c0-12.826-2.13-20.948-10.67-29.071-9.39-8.978-22.62-14.964-39.69-14.964-35 0-61.89 29.072-61.89 76.954 0 47.883 24.76 76.099 58.05 76.099M1455.92 199.372c0-7.268-5.97-13.253-13.23-13.253h-32.44v-38.477h32.44c7.26 0 13.23-5.985 13.23-13.253v-5.986c0-45.744 23.48-68.403 69.15-68.403h29.02v38.477h-29.45c-17.5 0-26.46 9.833-26.46 29.926v5.986c0 7.268 5.97 13.253 13.23 13.253h42.68v38.477h-42.68c-7.26 0-13.23 5.985-13.23 13.253v176.994h-42.26zM1652.02 381.496c-35.85 0-69.14-23.086-69.14-75.671V147.642h42.25v150.06c0 29.499 17.07 44.889 37.13 44.889 21.77 0 35.85-11.97 50.79-29.499 7.26-8.977 9.82-19.238 9.82-30.354V147.642h42.25v228.724h-42.25V356.7c0-5.131-6.4-6.841-10.24-2.993-13.24 12.826-31.59 27.789-60.61 27.789M1893.57 381.496c-38.84 0-79.39-19.239-90.06-65.838h43.54c6.4 17.528 23.9 29.498 44.81 29.498 23.05 0 37.13-13.68 37.13-30.353 0-16.246-11.09-25.224-28.59-30.354l-36.28-10.261c-31.58-8.978-55.06-29.499-55.06-64.556 0-38.049 35.43-67.12 75.55-67.12 32.01 0 70.85 14.535 81.09 65.41h-40.55c-5.55-17.528-20.06-29.071-40.54-29.071-20.06 0-34.58 12.398-34.58 28.216 0 13.253 8.11 23.086 27.32 28.644l34.15 9.833c32.43 9.406 58.47 29.072 58.47 66.693 0 39.332-34.15 69.259-76.4 69.259M2098.54 381.496c-61.46 0-102.01-51.73-102.01-119.706s43.11-119.278 101.58-119.278c63.6 0 96.89 51.302 96.89 109.872v23.087h-144.26c-5.98 0-8.54 3.847-7.26 13.68 4.7 32.064 30.31 54.295 55.49 54.295 18.78 0 35-9.405 45.67-27.788h44.81c-16.22 40.187-49.94 65.838-90.91 65.838m43.11-141.51c6.83 0 9.39-3.42 7.68-14.108-4.69-26.506-24.33-45.317-51.22-45.317-25.6 0-47.37 18.811-54.2 45.745-2.56 9.833.85 13.68 6.83 13.68z\"></path><path fill=\"#FF5D5F\" d=\"m286.292 286.105 34.597 27.791s26.473-19.661 45.941-22.545c20.418-3.025 42.202 8.359 62.388 21.93 30.489 20.498 56.149 46.508 56.149 46.508l30.06-29.493s-82.879-89.795-148.597-81.672c-43.105 5.328-80.538 37.481-80.538 37.481\"></path><path fill=\"#4E9CFF\" d=\"M88.358 114.862 60 146.056s79.009 73.732 141.224 73.732c28.358 0 67.684-22.216 101.523-51.079 19.283-16.448 40.835-35.13 62.388-35.13 14.487 0 33.594 7.673 51.612 27.824 0 0 11.63-6.974 18.716-11.985 6.228-4.404 15.479-11.91 15.479-11.91-25.918-27.663-63.407-47.883-85.807-45.9-36.299.005-62.388 22.601-94.717 48.735s-45.94 36.907-69.194 36.907c-39.134 0-112.866-62.388-112.866-62.388M88.358 352.463 60 321.269s79.009-73.732 141.224-73.732c28.358 0 67.684 22.216 101.523 51.079 19.283 16.448 40.835 35.13 62.388 35.13 14.556 0 33.518-7.989 51.612-28.358 0 0 10.877 6.705 17.582 11.344 6.894 4.769 17.015 12.655 17.015 12.655-25.931 27.883-63.693 48.323-86.209 46.33-36.299-.005-57.851-19.24-90.179-45.374-32.329-26.133-50.478-40.268-73.732-40.268-39.134 0-112.866 62.388-112.866 62.388M458.142 185.149c-7.378 5.1-19.283 12.478-19.283 12.478s6.806 14.746 6.806 34.597-6.239 36.866-6.239 36.866 10.688 6.675 17.582 11.343c7.162 4.849 18.149 13.045 18.149 13.045s13.045-27.224 13.045-61.254-13.045-59.552-13.045-59.552-10.236 7.792-17.015 12.477\"></path><path fill=\"#FF5D5F\" d=\"m287.995 180.612 32.895-27.224s26.473 19.046 45.941 21.93c20.417 3.026 42.202-8.359 62.388-21.93 30.489-20.498 56.149-46.507 56.149-46.507l30.06 29.492s-82.879 89.795-148.597 81.672c-43.105-5.328-78.836-37.433-78.836-37.433M208.601 91c42.538 0 78.264 36.299 78.264 36.299s-9.941 7.832-16.448 13.045c-6.777 5.429-17.582 14.179-17.582 14.179s-18.711-19.851-44.234-19.851c-10.465 0-24.066 6.286-38.567 18.716-11.188 9.591-22.829 21.514-30.627 36.299-6.743 12.784-10.42 27.85-10.776 43.672-.446 19.873 6.597 40.704 18.149 57.283 7.743 11.112 16.983 19.474 26.657 26.657 12.555 9.322 25.648 15.881 35.164 15.881 10.166 0 19.306-3.533 26.09-6.806 10.776-6.239 19.278-13.612 19.278-13.612l33.463 27.791s-13.612 13.612-32.323 23.821c-12.091 5.963-27.632 11.91-46.508 11.91-18.862 0-40.767-10.022-61.254-25.522-13.244-10.021-26.225-21.895-36.298-36.299-16.51-23.607-25.017-52.328-24.96-81.104.057-29.136 9.451-57.993 26.094-81.672C138.273 117.657 176.86 91 208.601 91\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">3</span><span class=\"skillTracker__skill\" data-state=\"current\">Identity in front of every port</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\">Part 1</a> closed the ports nobody meant to\nopen. <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/\">Part 2</a> stopped two containers\nfrom running as root. Both were about the machine. Neither touched the question a\nself-hosted AI stack answers worst: <strong>who is allowed to log in, and where is that\ndecided?</strong></p>\n<p>Same box as before. The Langfuse we've been hardening since Part 1 — the one whose\nPostgres, ClickHouse and Redis Part 1 found correctly bound to localhost, and whose\nworker Part 2 dropped off root — was deployed back in\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/\">Catch what your tests miss</a>. Two parts\nhave now secured the machine underneath it without once touching the application's own\nfront door. This is the first part that changes Langfuse itself.</p>\n<p>Right now that decision is made in each app, separately. Langfuse has its own email-and-\npassword table. So does every other tool on the box. Each one is a place where an account\ncan outlive the person who owned it, where a password can be reused, and where \"remove\nthis person's access\" means remembering that the app exists at all.</p>\n<p>This post moves that decision to one place. That place is\n<a href=\"https://goauthentik.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">authentik</a> — an open-source identity provider you run\nyourself, the self-hosted counterpart to Okta or Auth0. It holds the accounts, runs the\nlogin screen, and vouches for who someone is to any app that asks. Apps stop storing\npasswords and start asking authentik.</p>\n<p>It's the same job Keycloak does, and Keycloak is the better-known name. authentik earns\nthe pick here on setup cost: a compose file and a wizard against Keycloak's realms,\nclients and JVM tuning. For one box and one app, that difference is the whole decision.</p>\n<p>We deploy it, connect Langfuse to it over OIDC, and end with a login that goes through\nauthentik and back.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>One WEC Instance: 8 vCPU AMD EPYC 7601, 15 GB RAM, Docker 29.1.3, kernel 5.15.\nauthentik <code>2026.8.1</code> from <code>ghcr.io/goauthentik/server</code>, Postgres 16. Langfuse v3\n(<code>langfuse/langfuse:3</code>) — the deployment carried over from Parts 1 and 2, reachable at\n<code>10.80.4.212:3001</code>. authentik published on host port <code>9100</code>.</p><p>Addresses in this post are a private LAN. Substitute yours. Everything here is plain\nHTTP on a trusted network, which is fine for a first run and <strong>not</strong> fine for anything\nreachable from outside — see <a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#whats-next\" class=\"\">What's next</a>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-vocabulary-before-the-clicking\">The vocabulary, before the clicking<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#the-vocabulary-before-the-clicking\" class=\"hash-link\" aria-label=\"Direct link to The vocabulary, before the clicking\" title=\"Direct link to The vocabulary, before the clicking\" translate=\"no\">​</a></h2>\n<p>Every OIDC guide assumes you already know these five words. Getting them straight up\nfront is the difference between configuring this once and guessing at fields for an\nhour.</p>\n<p><strong>OP and RP.</strong> The <strong>OpenID Provider</strong> is the thing that knows who people are —\nauthentik. The <strong>Relying Party</strong> is the app that wants to be told — Langfuse. The RP\nnever sees a password. It receives a signed statement from the OP saying \"this is\n<code>admin@example.com</code>, I checked.\"</p>\n<p><strong>Provider vs application.</strong> authentik splits what most tools merge. A <strong>provider</strong> is\nthe protocol endpoint — the OIDC machinery, the client ID and secret, the redirect URI.\nAn <strong>application</strong> is the thing users see and the thing access rules attach to. They\npair one-to-one: one application, one provider, and the wizard in Step 3 creates both\ntogether.</p>\n<p><strong>The redirect URI</strong> is where authentik is allowed to send the user back after a\nsuccessful login. It's matched exactly and it's the single most common source of\nfailure. Not a prefix, not a hostname — the full URL, character for character.</p>\n<p><strong>Scopes vs grant types.</strong> A <strong>scope</strong> is what information the app asks for\n(<code>openid email profile</code>). A <strong>grant type</strong> is the mechanism by which it asks\n(<code>authorization_code</code>). Different questions; the UI puts them near each other and it's\neasy to conflate them.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-the-login-actually-does\">What the login actually does<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#what-the-login-actually-does\" class=\"hash-link\" aria-label=\"Direct link to What the login actually does\" title=\"Direct link to What the login actually does\" translate=\"no\">​</a></h2>\n<!-- -->\n<p>The part worth noticing: the code travels through the browser, the secret never does.\nThat exchange in the second-to-last step happens between the two containers directly,\nwhich is why the client secret is a server-side setting and why Langfuse must be able to\nreach authentik over the network — not just your browser.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">A running Langfuse, ideally the one from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\">Part 1</a></li>\n<li class=\"\">Docker with the Compose plugin</li>\n<li class=\"\">A free host port for authentik (we use <code>9100</code>)</li>\n<li class=\"\">The LAN IP of the box, not <code>localhost</code> — two containers need to reach each other and\nyour browser needs to reach both</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--deploy-authentik\">Step 1 — Deploy authentik<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#step-1--deploy-authentik\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Deploy authentik\" title=\"Direct link to Step 1 — Deploy authentik\" translate=\"no\">​</a></h2>\n<p>authentik ships a compose file and generates its own secrets. Create a directory and\npull both down:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-O</span><span class=\"token plain\"> https://goauthentik.io/docker-compose.yml</span><br></div></code></pre></div></div>\n<p>Now the environment file. authentik needs a Postgres password and a secret key, both\nrandom, plus the ports it will publish:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"PG_PASS=</span><span class=\"token string variable\" style=\"color:#36acaa\">$(</span><span class=\"token string variable\" style=\"color:#36acaa\">openssl rand </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-base64</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable number\" style=\"color:#36acaa\">36</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable operator\" style=\"color:#393A34\">|</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable function\" style=\"color:#d73a49\">tr</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable string\" style=\"color:#e3116c\">'\\n'</span><span class=\"token string variable\" style=\"color:#36acaa\">)</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"AUTHENTIK_SECRET_KEY=</span><span class=\"token string variable\" style=\"color:#36acaa\">$(</span><span class=\"token string variable\" style=\"color:#36acaa\">openssl rand </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-base64</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable number\" style=\"color:#36acaa\">60</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable operator\" style=\"color:#393A34\">|</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable function\" style=\"color:#d73a49\">tr</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable string\" style=\"color:#e3116c\">'\\n'</span><span class=\"token string variable\" style=\"color:#36acaa\">)</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"COMPOSE_PORT_HTTP=9100\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"COMPOSE_PORT_HTTPS=9543\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> .env</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">chmod</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">600</span><span class=\"token plain\"> .env</span><br></div></code></pre></div></div>\n<p><code>AUTHENTIK_SECRET_KEY</code> signs sessions and tokens. If you lose it, every existing session\nand every issued token becomes invalid — so it belongs in your password manager, not\nonly in this file.</p>\n<p>Bring it up. First boot runs database migrations and takes a minute or two:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><br></div></code></pre></div></div>\n<p>Three containers — <code>server</code>, <code>worker</code> and <code>postgresql</code> — all reporting <code>(healthy)</code>.\nOlder authentik compose files also shipped a separate <code>redis</code> service; <code>2026.8.1</code> no\nlonger does, so three is correct and nothing is missing. <code>server</code> is the one that has to\nbe healthy before you continue.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker compose ps showing authentik&amp;#39;s server, worker and postgresql containers all reporting healthy\" src=\"https://development-wec.wiline.com/docs/assets/images/authentik-compose-ps-2cdcd74dfeb2be5f3c8d9bb137b646db.png\" width=\"1127\" height=\"102\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--claim-the-admin-account\">Step 2 — Claim the admin account<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#step-2--claim-the-admin-account\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Claim the admin account\" title=\"Direct link to Step 2 — Claim the admin account\" translate=\"no\">​</a></h2>\n<p>authentik's first boot leaves the admin account unclaimed. The setup URL exists exactly\nonce; visiting it is how you take ownership.</p>\n<p>Open <code>http://10.80.4.212:9100/if/flow/initial-setup/</code>, and set the email and password\nfor the default admin (<code>akadmin</code>).</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The authentik initial-setup screen, setting the email and password for the default akadmin account\" src=\"https://development-wec.wiline.com/docs/assets/images/authentik-initial-setup-e170a5fefdbc3d6a5b645819857b6cae.png\" width=\"1900\" height=\"955\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Use an address you control. This account can do everything, so give it a real password\nand store it somewhere durable.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>This URL is a one-time key</div><div class=\"admonitionContent_BuS1\"><p>Until someone completes this form, anyone who can reach the port can become the\nadministrator of your identity provider. Do it immediately after the containers come up\n— not tomorrow.</p></div></div>\n<p>You land on the Application Dashboard — the user-facing view, not the admin one, and empty\nbecause nothing is configured yet.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The authentik Application Dashboard reading No Applications available, with a Create a new application button\" src=\"https://development-wec.wiline.com/docs/assets/images/authentik-app-dashboard-empty-fb4aaff0a94a00ac91abe85b9541079d.png\" width=\"1902\" height=\"954\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><code>Create a new application</code> here goes to the same place as the admin interface does; the\n<strong>Admin interface</strong> button top-right is how you reach everything else.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--run-the-application-wizard\">Step 3 — Run the application wizard<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#step-3--run-the-application-wizard\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — Run the application wizard\" title=\"Direct link to Step 3 — Run the application wizard\" translate=\"no\">​</a></h2>\n<p><strong>Applications → Applications → Create</strong>. authentik walks you through five steps, and\nthe step list on the left is worth reading before you touch anything:</p>\n<p><strong>Application → Choose a Provider → Configure Provider → Configure Bindings → Review and\nSubmit</strong></p>\n<p>That's the vocabulary from earlier, in order. The wizard creates the application and its\nprovider in one pass and pairs them for you. (<code>Applications → Providers → Create</code> builds\na provider on its own, if you ever need one before the app that uses it.)</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"31--application\">3.1 — Application<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#31--application\" class=\"hash-link\" aria-label=\"Direct link to 3.1 — Application\" title=\"Direct link to 3.1 — Application\" translate=\"no\">​</a></h3>\n<ul>\n<li class=\"\"><strong>Application Name</strong> — <code>Langfuse</code>. The label users see on the dashboard.</li>\n<li class=\"\"><strong>Slug</strong> — <code>langfuse</code>. Load-bearing: it becomes part of the issuer URL, so changing it\nlater changes the URL you have to reconfigure Langfuse with.</li>\n<li class=\"\"><strong>Group</strong> — leave empty. This groups apps on the dashboard; it has nothing to do with\nuser groups or access control, despite the name.</li>\n<li class=\"\"><strong>Policy engine mode</strong> — leave on <strong>ANY</strong>. It decides how multiple bound policies\ncombine. With no policies bound, it makes no difference yet.</li>\n</ul>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The wizard&amp;#39;s Application step, showing the Name, Slug, Group and Policy engine mode fields before anything is entered\" src=\"https://development-wec.wiline.com/docs/assets/images/authentik-wizard-application-62b3555c7b9b05dedfdd535153d2c084.png\" width=\"1901\" height=\"960\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"32--choose-a-provider\">3.2 — Choose a Provider<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#32--choose-a-provider\" class=\"hash-link\" aria-label=\"Direct link to 3.2 — Choose a Provider\" title=\"Direct link to 3.2 — Choose a Provider\" translate=\"no\">​</a></h3>\n<p>Eight provider types. Take <strong>OAuth2/OpenID Provider</strong> — \"OAuth2 Provider for generic\nOAuth and OpenID Connect Applications.\"</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The wizard&amp;#39;s provider type step with eight tiles and OAuth2/OpenID Provider selected\" src=\"https://development-wec.wiline.com/docs/assets/images/authentik-wizard-provider-type-62af4193167bfaa5359808b786af8d41.png\" width=\"1898\" height=\"958\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Worth knowing what you're <em>not</em> picking, because two of these solve a different problem:\n<strong>Proxy Provider</strong> puts authentik in front of an app that has no SSO support of its own,\nand <strong>LDAP Provider</strong> exposes authentik to things that only speak LDAP. Langfuse speaks\nOIDC natively, so it gets a real OIDC provider.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"33--configure-provider\">3.3 — Configure Provider<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#33--configure-provider\" class=\"hash-link\" aria-label=\"Direct link to 3.3 — Configure Provider\" title=\"Direct link to 3.3 — Configure Provider\" translate=\"no\">​</a></h3>\n<p>The step that matters, and the one that will cost you time if you rush it.</p>\n<ul>\n<li class=\"\">\n<p><strong>Authorization flow</strong> — <code>default-provider-authorization-explicit-consent</code>. \"Explicit\"\nshows the user a consent screen listing what Langfuse asked for. The implicit variant\nskips it. Pick explicit for a first build: that screen is a live readout of your scope\nconfiguration, which makes a misconfiguration visible instead of silent.</p>\n</li>\n<li class=\"\">\n<p><strong>Client type</strong> — <strong>Confidential</strong> (scrolled above the redirect fields). Langfuse is a\nserver and can keep a secret, so it gets one. Public clients are for browsers and\nmobile apps that can't.</p>\n</li>\n<li class=\"\">\n<p><strong>Redirect URIs/Origins (RegEx)</strong> — the field with two dropdowns and a value. Set the\nmode to <strong>Strict</strong>, the purpose to <strong>Authorization</strong>, and the URI to Langfuse's\nNextAuth callback:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">http://10.80.4.212:3001/api/auth/callback/custom</span><br></div></code></pre></div></div>\n<p>The trailing <code>custom</code> is not a placeholder — it's the provider ID NextAuth assigns to\nits generic OIDC provider. Use your own host and port, but that path is fixed.</p>\n</li>\n<li class=\"\">\n<p><strong>Signing Key</strong> — leave <code>authentik Self-signed Certificate</code>. This signs the ID token;\nLangfuse fetches the matching public key from the <code>jwks_uri</code> in Step 5 and verifies\nagainst it. Self-signed is correct here — the RP trusts this key because discovery\nhanded it over, not because a CA vouched for it.</p>\n</li>\n<li class=\"\">\n<p><strong>Logout URI</strong> and token validities — leave alone.</p>\n</li>\n</ul>\n<p>The provider once it's saved — explicit-consent authorization flow, Confidential client\ntype, and Authorization Code as the grant:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The OAuth2/OpenID provider edit form showing the explicit-consent authorization flow, Client Type set to Confidential, and Authorization Code checked\" src=\"https://development-wec.wiline.com/docs/assets/images/authentik-wizard-configure-provider-11b9bd02c65bf52f23d2afe34f3a8be9.png\" width=\"1896\" height=\"948\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Strict means strict</div><div class=\"admonitionContent_BuS1\"><p>The mode dropdown offers <strong>Regex</strong> as well, and authentik's own help text points out you\ncan set it to <code>.*</code> to allow any redirect URI. Don't. An open redirect URI means anyone\nwho can start a login can have the authorization code delivered to a host they control.\nStrict, with one exact URL, is the whole point of the field.</p></div></div>\n<p>Further down, <strong>Scopes</strong> comes pre-populated with four mappings: <code>email</code>, <code>openid</code>,\n<code>profile</code> and <code>offline_access</code>. Leave them. <code>openid</code> is mandatory and is what marks this\nas OIDC rather than bare OAuth2. <code>email</code> is the claim Langfuse keys the account on.\n<code>profile</code> carries the display name. <code>offline_access</code> is the one the wizard adds without\nasking — it permits a refresh token, so a session can be renewed without sending the user\nback through the login flow.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"34--configure-bindings\">3.4 — Configure Bindings<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#34--configure-bindings\" class=\"hash-link\" aria-label=\"Direct link to 3.4 — Configure Bindings\" title=\"Direct link to 3.4 — Configure Bindings\" translate=\"no\">​</a></h3>\n<p><code>No bound policies.</code> — and we are leaving it that way.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The wizard&amp;#39;s Configure Bindings step reading No bound policies\" src=\"https://development-wec.wiline.com/docs/assets/images/authentik-wizard-bindings-b5cb6f9b81084f2eb498d0b4cdeae6cd.png\" width=\"1896\" height=\"954\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>This is the gap, and it's deliberate</div><div class=\"admonitionContent_BuS1\"><p>Read the wizard's own description: <em>\"These policies control which users can access this\napplication.\"</em> With nothing bound, the answer is <strong>everyone who can authenticate</strong>. On a\nbox where you're the only account, that's the same thing as \"just me\" — which is why\nit's tolerable here and why the wizard lets you skip it.</p><p>It is not access control. Every account you ever create in this authentik gets into\nLangfuse by default, and nothing will warn you. Groups and policy bindings are how that\ngets fixed, and they're the subject of the next post — where the thing being protected\nis a gateway and the answer matters a great deal more.</p></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"35--review-and-submit\">3.5 — Review and Submit<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#35--review-and-submit\" class=\"hash-link\" aria-label=\"Direct link to 3.5 — Review and Submit\" title=\"Direct link to 3.5 — Review and Submit\" translate=\"no\">​</a></h3>\n<p>Check the slug and the redirect URI once more, then submit.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--read-the-configuration-back-out-of-authentik\">Step 4 — Read the configuration back out of authentik<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#step-4--read-the-configuration-back-out-of-authentik\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Read the configuration back out of authentik\" title=\"Direct link to Step 4 — Read the configuration back out of authentik\" translate=\"no\">​</a></h2>\n<p>Rather than assembling URLs by hand, ask authentik. Every OIDC provider publishes a\ndiscovery document, and authentik's lives under the application slug:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://10.80.4.212:9100/application/o/langfuse/.well-known/openid-configuration </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'{issuer, authorization_endpoint, token_endpoint, userinfo_endpoint, jwks_uri, grant_types_supported}'</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"issuer\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://10.80.4.212:9100/application/o/langfuse/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"authorization_endpoint\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://10.80.4.212:9100/application/o/authorize/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"token_endpoint\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://10.80.4.212:9100/application/o/token/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"userinfo_endpoint\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://10.80.4.212:9100/application/o/userinfo/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"jwks_uri\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://10.80.4.212:9100/application/o/langfuse/jwks/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"grant_types_supported\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"authorization_code\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"refresh_token\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"implicit\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"client_credentials\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"password\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"urn:ietf:params:oauth:grant-type:device_code\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>Two things to take from this. The <strong>issuer</strong> is the only URL Langfuse needs — it will\nfetch this same document and discover the rest. And <code>grant_types_supported</code> lists what\nthe protocol offers, not what your provider will accept; we're using\n<code>authorization_code</code> and the presence of <code>password</code> in that list is not an invitation.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The discovery document returned by the running authentik, showing the issuer, the authorize, token and userinfo endpoints, the JWKS URL and the supported grant types\" src=\"https://development-wec.wiline.com/docs/assets/images/authentik-discovery-json-959875837069e04a0b42b7cc81ad0b75.png\" width=\"860\" height=\"313\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Now the client credentials. They're in the provider's detail page in the UI, but\n<code>docker compose exec</code> is faster and copy-pastes cleanly:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/authentik </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> server ak shell </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from authentik.core.models import Application</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">p = Application.objects.get(slug='langfuse').get_provider()</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('PROVIDER     :', p.name)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('CLIENT_ID    :', p.client_id)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print('CLIENT_SECRET:', p.client_secret)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div></code></pre></div></div>\n<p>Note that this looks the provider up <em>through the application slug</em> rather than by\nprovider name. The wizard named your provider <code>Provider for Langfuse</code>, not <code>langfuse</code> —\nthe slug is the thing you chose and the thing that stays predictable. <code>get_provider()</code>\nalso matters: reaching for <code>application.provider</code> hands back the base class with\n<code>client_id</code> empty, which looks alarmingly like a broken provider and isn't.</p>\n<p>Keep that output out of your shell history and out of screenshots.</p>\n<p>The provider's detail page carries the same values, if you'd rather read them there —\nclient type, client ID, the strict redirect URI, and every discovery URL in one place.\nThe client secret is the one thing it doesn't print:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Provider for Langfuse overview page listing client type, client ID, the strict redirect URI and the OpenID configuration, authorize, token, userinfo and JWKS URLs\" src=\"https://development-wec.wiline.com/docs/assets/images/authentik-provider-overview-5b451791d184236f85921a5a394f956e.png\" width=\"1897\" height=\"952\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--point-langfuse-at-it\">Step 5 — Point Langfuse at it<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#step-5--point-langfuse-at-it\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — Point Langfuse at it\" title=\"Direct link to Step 5 — Point Langfuse at it\" translate=\"no\">​</a></h2>\n<p>Langfuse reads generic OIDC settings from <code>AUTH_CUSTOM_*</code> variables. Add them to\n<code>~/langfuse/.env</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">AUTH_CUSTOM_NAME</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">authentik</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">AUTH_CUSTOM_ISSUER</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">http://10.80.4.212:9100/application/o/langfuse</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">AUTH_CUSTOM_CLIENT_ID</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">the client </span><span class=\"token function\" style=\"color:#d73a49\">id</span><span class=\"token plain\"> from Step </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">4</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">AUTH_CUSTOM_CLIENT_SECRET</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">the client secret from Step </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">4</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">AUTH_CUSTOM_SCOPE</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">openid email profile</span><br></div></code></pre></div></div>\n<p>The issuer here has no trailing slash while the discovery document reported one. Both\nwork — Langfuse normalizes it.</p>\n<p>Also confirm <code>NEXTAUTH_URL</code> matches the address you actually browse to:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> NEXTAUTH_URL ~/langfuse/.env</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">NEXTAUTH_URL=http://10.80.4.212:3001</span><br></div></code></pre></div></div>\n<p>If this says <code>localhost</code> while your redirect URI says <code>10.80.4.212</code>, the callback will\nbe built with the wrong hostname and authentik will reject it.</p>\n<p><strong>Now the part that will waste your afternoon if you skip it.</strong> Writing those variables\ninto <code>.env</code> is not enough. Langfuse's shipped compose file lists the variables it passes\ninto the <code>langfuse-web</code> container explicitly, and <code>AUTH_CUSTOM_*</code> isn't among them —\nthey land in <code>.env</code>, get read by Compose for interpolation, and never reach the process.\nLangfuse starts fine and shows no SSO button, with nothing in the logs to say why.</p>\n<p>Pass them through with an override file, <code>~/langfuse/docker-compose.override.yml</code>:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">services</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">langfuse-web</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">environment</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">AUTH_CUSTOM_NAME</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">AUTH_CUSTOM_NAME</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">AUTH_CUSTOM_ISSUER</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">AUTH_CUSTOM_ISSUER</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">AUTH_CUSTOM_CLIENT_ID</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">AUTH_CUSTOM_CLIENT_ID</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">AUTH_CUSTOM_CLIENT_SECRET</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">AUTH_CUSTOM_CLIENT_SECRET</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">AUTH_CUSTOM_SCOPE</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">AUTH_CUSTOM_SCOPE</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">AUTH_CUSTOM_ALLOW_ACCOUNT_LINKING</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"true\"</span><br></div></code></pre></div></div>\n<p>Compose merges <code>docker-compose.override.yml</code> automatically, so the upstream file stays\nuntouched and survives upgrades. That last variable is the fix for a failure you'd\notherwise hit in the next step — the troubleshooting section explains what it does and\nwhat it costs.</p>\n<p>Recreate the web container and verify the variables arrived:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/langfuse </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> langfuse-web</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/langfuse </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> langfuse-web </span><span class=\"token function\" style=\"color:#d73a49\">env</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> AUTH_CUSTOM</span><br></div></code></pre></div></div>\n<p>Six. If it's zero, the override isn't being picked up — check you're running compose\nfrom <code>~/langfuse</code> and that the filename is exactly <code>docker-compose.override.yml</code>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker compose exec counting six AUTH_CUSTOM variables inside the langfuse-web container\" src=\"https://development-wec.wiline.com/docs/assets/images/langfuse-env-count-c57cd6125a9c2dbc129ef850beee6378.png\" width=\"477\" height=\"82\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-6--sign-in\">Step 6 — Sign in<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#step-6--sign-in\" class=\"hash-link\" aria-label=\"Direct link to Step 6 — Sign in\" title=\"Direct link to Step 6 — Sign in\" translate=\"no\">​</a></h2>\n<p>Open <code>http://10.80.4.212:3001/auth/sign-in</code>, in a private window so you're not looking at\nan existing session. There's a new button below the divider under the password form.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Langfuse sign-in page with an authentik button below the email and password form\" src=\"https://development-wec.wiline.com/docs/assets/images/langfuse-sso-button-40985ca287c0686b8a2c65f8b7998d3d.png\" width=\"2838\" height=\"1634\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>The button is labelled <code>authentik</code> because that is your <code>AUTH_CUSTOM_NAME</code> value, rendered\nverbatim. Set it to <code>Company SSO</code> and that's what the button says — it's a display string\nand nothing else depends on it.</p>\n<p>Two other things in that screenshot are worth naming rather than glossing over. The\nemail-and-password form is still there, and so is <strong>Sign up</strong> — SSO has been <em>added</em>, not\nsubstituted. And the address bar says <strong>Not Secure</strong>, correctly: this is plain HTTP.</p>\n<p>Click the button and you're handed to authentik, which tells you where you came from:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The authentik login screen reading Log in to continue to Langfuse\" src=\"https://development-wec.wiline.com/docs/assets/images/authentik-login-langfuse-f20d6f5f65651cb88e1db77bdd2f7c07.png\" width=\"1900\" height=\"987\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Log in, and because we chose the explicit-consent flow in Step 3.3, authentik shows what\nLangfuse asked for before it hands anything over:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The authentik consent screen, Redirecting to Langfuse, listing Email address and General Profile Information\" src=\"https://development-wec.wiline.com/docs/assets/images/authentik-consent-1db931f804b8b9ddaa8562e01b588adf.png\" width=\"1900\" height=\"990\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>It reads <code>You're about to sign into Langfuse</code>, identifies you as <code>akadmin</code>, and lists two\npermissions — <strong>Email address</strong> and <strong>General Profile Information</strong>. Those are the <code>email</code>\nand <code>profile</code> scopes in human-readable form. <code>openid</code> doesn't get a line of its own: it\ncarries no personal data to consent to, it's the flag that makes this OIDC rather than\nplain OAuth2. So a three-scope configuration produces a two-item consent screen, which is\ncorrect and not a sign that something dropped.</p>\n<p>Continue, and you land in Langfuse, signed in, as the identity authentik vouched for.</p>\n<p>That's the loop closed. Langfuse never saw a password.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting--the-errors-this-run-actually-produced\">Troubleshooting — the errors this run actually produced<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#troubleshooting--the-errors-this-run-actually-produced\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting — the errors this run actually produced\" title=\"Direct link to Troubleshooting — the errors this run actually produced\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"oauthaccountnotlinked\"><code>OAuthAccountNotLinked</code><a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#oauthaccountnotlinked\" class=\"hash-link\" aria-label=\"Direct link to oauthaccountnotlinked\" title=\"Direct link to oauthaccountnotlinked\" translate=\"no\">​</a></h3>\n<p>The first sign-in attempt bounced back to the Langfuse login page with\n<code>?error=OAuthAccountNotLinked</code> in the URL and nothing more.</p>\n<p>The cause: an email-and-password Langfuse account already existed with the same address\nauthentik was now presenting. NextAuth's default is to refuse the merge. That default is\nprotective — if an identity provider can assert any email and the app auto-links on\nemail alone, then whoever controls the provider can take over an existing account by\nclaiming its address.</p>\n<p><code>AUTH_CUSTOM_ALLOW_ACCOUNT_LINKING: \"true\"</code> tells Langfuse to link them anyway. It is\nsafe <strong>here</strong> because you own the only provider and you control who can create accounts\nin it. It would not be safe pointed at a provider where anyone can self-register an\narbitrary email.</p>\n<p>The alternative, if you'd rather not enable it: delete the pre-existing local account\nand let SSO create a fresh one.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"redirect_uri-mismatch-or-authentik-refusing-to-redirect\"><code>redirect_uri</code> mismatch, or authentik refusing to redirect<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#redirect_uri-mismatch-or-authentik-refusing-to-redirect\" class=\"hash-link\" aria-label=\"Direct link to redirect_uri-mismatch-or-authentik-refusing-to-redirect\" title=\"Direct link to redirect_uri-mismatch-or-authentik-refusing-to-redirect\" translate=\"no\">​</a></h3>\n<p>Read the failing URL in the address bar and compare it to the provider's redirect URI\ncharacter by character. In this run the mismatches worth naming were the trailing\n<code>/api/auth/callback/custom</code> path (any other suffix fails), <code>localhost</code> versus the LAN IP,\nand a stray trailing slash. Strict matching means strict.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"no-sso-button-no-errors\">No SSO button, no errors<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#no-sso-button-no-errors\" class=\"hash-link\" aria-label=\"Direct link to No SSO button, no errors\" title=\"Direct link to No SSO button, no errors\" translate=\"no\">​</a></h3>\n<p>That's the compose-environment trap from Step 5. Check the variables reached the\ncontainer:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/langfuse </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> langfuse-web </span><span class=\"token function\" style=\"color:#d73a49\">env</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> AUTH_CUSTOM_ISSUER</span><br></div></code></pre></div></div>\n<p>Empty output means Langfuse is running without the config it never knew it was missing.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"discovery-fails-from-inside-the-container\">Discovery fails from inside the container<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#discovery-fails-from-inside-the-container\" class=\"hash-link\" aria-label=\"Direct link to Discovery fails from inside the container\" title=\"Direct link to Discovery fails from inside the container\" translate=\"no\">​</a></h3>\n<p>Your browser reaching authentik is not proof that Langfuse can. The token exchange is\ncontainer-to-container:</p>\n<p>The <code>langfuse-web</code> image ships no <code>curl</code>, so use the Node runtime that's already in\nthere — <code>fetch</code> is built in:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/langfuse </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> langfuse-web </span><span class=\"token function\" style=\"color:#d73a49\">node</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token string\" style=\"color:#e3116c\">\"fetch('http://10.80.4.212:9100/application/o/langfuse/.well-known/openid-configuration').then(r=&gt;console.log(r.status)).catch(e=&gt;console.log('FAIL',e.message))\"</span><br></div></code></pre></div></div>\n<p><code>200</code> is what you want. <code>FAIL</code> with a connection error means the container can't reach\nauthentik at all, which is the actual thing being tested. This is also why the issuer is a LAN IP rather than <code>localhost</code>\n— inside the Langfuse container, <code>localhost</code> is the Langfuse container.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The node fetch call run inside langfuse-web printing 200\" src=\"https://development-wec.wiline.com/docs/assets/images/langfuse-discovery-200-f17cc0564e966cf97ba8d8a4cc9e7c9f.png\" width=\"533\" height=\"120\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can run your own identity provider and put an app behind it over OIDC — reading the\nissuer, client ID and secret out of authentik rather than assembling URLs by hand, and\nrecognising the two failures that actually stop you: a compose file that never passes the\nvariables through, and <code>OAuthAccountNotLinked</code>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-this-did-and-didnt-buy-you\">What this did and didn't buy you<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#what-this-did-and-didnt-buy-you\" class=\"hash-link\" aria-label=\"Direct link to What this did and didn't buy you\" title=\"Direct link to What this did and didn't buy you\" translate=\"no\">​</a></h2>\n<p>Done: one identity provider, one place where accounts live, one place to revoke them.\nLangfuse no longer stores a password for you. And you now have a provider you can point\nthe next app at in about five minutes.</p>\n<p>Not done, and worth being clear about:</p>\n<ul>\n<li class=\"\"><strong>No access control.</strong> Empty bindings mean every authentik account reaches Langfuse.</li>\n<li class=\"\"><strong>Plain HTTP.</strong> The ID token and the client secret cross the network unencrypted. On a\ntrusted LAN that's a tolerable starting point; exposed to anything else it isn't.</li>\n<li class=\"\"><strong>One admin, no recovery path.</strong> If you lose <code>akadmin</code>, you lose the provider that\nfronts everything pointed at it.</li>\n<li class=\"\"><strong>The old login still works.</strong> Password sign-in wasn't disabled, so SSO is currently an\nadditional door, not a replacement one.</li>\n</ul>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Identity in front of every port</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>Part 4 puts identity in front of something where these gaps stop being tolerable: the\nLiteLLM gateway from the <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\">gateway series</a>.\nThree things this post deliberately left alone:</p>\n<ul>\n<li class=\"\"><strong>The control plane is not the data plane.</strong> SSO protects an admin UI. It does not\nprotect an API — machine callers still authenticate with keys. For a gateway that\ndistinction is the whole game, and assuming otherwise is how people conclude they've\nsecured something they haven't.</li>\n<li class=\"\"><strong>Groups and policy bindings</strong>, so that reaching the application requires membership\nrather than merely having an account.</li>\n<li class=\"\"><strong>TLS and real exposure</strong> — the gateway is bound to <code>127.0.0.1:4000</code>, and getting it\nproperly reachable means a reverse proxy, a hostname and a certificate.</li>\n</ul>\n<p>The provider-and-application steps will be a short recap with a link back here. The rest\nis new.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.goauthentik.io/docs/add-secure-apps/providers/oauth2/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">authentik docs — OAuth2/OpenID provider</a></li>\n<li class=\"\"><a href=\"https://docs.goauthentik.io/docs/add-secure-apps/applications/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">authentik docs — applications, and binding policies to them</a></li>\n<li class=\"\"><a href=\"https://langfuse.com/self-hosting/authentication-and-sso\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Langfuse docs — SSO and the <code>AUTH_CUSTOM_*</code> variables</a></li>\n<li class=\"\"><a href=\"https://next-auth.js.org/configuration/providers/oauth#allowdangerousemailaccountlinking-option\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">NextAuth — account linking, and why <code>OAuthAccountNotLinked</code> exists</a></li>\n<li class=\"\"><a href=\"https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">OpenID Connect Core — the authorization code flow</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/",
            "title": "One login for everything: putting authentik in front of a self-hosted app",
            "summary": "Part 2 fixed how containers run. This fixes who gets to log in. Deploy authentik as your own identity provider, connect Langfuse to it over OIDC, and understand the pieces — provider, application, redirect URI, scopes — instead of copying a config. Includes the two failures a real run produced: a compose file that silently swallows your settings, and OAuthAccountNotLinked.",
            "date_modified": "2026-09-02T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "security",
                "sso",
                "oidc",
                "authentik",
                "identity",
                "langfuse",
                "self-hosting",
                "hardening"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__presidio\" src=\"data:image/webp;base64,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\" alt=\"Presidio\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 5 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->5</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->5<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting an LLM gateway</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">One endpoint, scoped keys</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Route work to the right model</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Mask PII at the gateway</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Clean traces, untouched answers</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">🏆</span><span class=\"skillTracker__skill\" data-state=\"current\">Prove it holds under load</span></span></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/\">Part 4</a> built a callback that masks\nPII in what the gateway logs without touching what the caller receives. It also noted\nthat the first working version used a blocking HTTP client inside an <code>async def</code>, and\nthat this cost 200-350ms across four sequential requests.</p>\n<p>That post ended with an admission: four requests in a row is the wrong test. A blocked\nevent loop barely shows when nothing else is waiting. The damage should appear under\nconcurrency, and we had not measured it.</p>\n<p>This is that measurement, and the result is a shape rather than a number. <strong>With the\nblocking client, some batches take many times longer than they should. With the async\nclient, none do.</strong> The medians barely differ, which is exactly why this ships unnoticed.</p>\n<p>Two things turned up that were not in the plan. The gateway does not merely slow down —\nit <strong>drops requests</strong>. And when it does, the masking call is the thing that timed out,\nwhile the caller still gets <code>200 OK</code>. That means the guarantee Part 4 was built on\nquietly stops holding under load.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-you-need\">What you need<a href=\"https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/#what-you-need\" class=\"hash-link\" aria-label=\"Direct link to What you need\" title=\"Direct link to What you need\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">The gateway from <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\">Part 1</a>, running</li>\n<li class=\"\">Presidio and the logging callback from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/\">Part 4</a></li>\n<li class=\"\">A WEC Inference API key — <a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/inference/api_keys/\">create one</a> in\nthe portal</li>\n<li class=\"\"><code>httpx</code> in your Python environment</li>\n</ul>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Where these numbers came from</div><div class=\"admonitionContent_BuS1\"><p>One WEC Instance: 8 vCPU AMD EPYC 7601, 15 GB RAM, Docker 29.1.3, kernel 5.15. LiteLLM\n<code>1.96.2</code> from <code>ghcr.io/berriai/litellm:main-stable</code>. Presidio analyzer and anonymizer as\nlocal containers on the same host. Model <code>Qwen2.5-3B-Instruct</code> over the WEC Inference\nAPI. Five repetitions per cell.</p><p>Latency numbers do not transfer between machines. Treat every second in this post as one\nobservation from that setup, not a figure to match. What should reproduce is the\ndifference between the two versions.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"first-find-out-how-many-event-loops-you-have\">First, find out how many event loops you have<a href=\"https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/#first-find-out-how-many-event-loops-you-have\" class=\"hash-link\" aria-label=\"Direct link to First, find out how many event loops you have\" title=\"Direct link to First, find out how many event loops you have\" translate=\"no\">​</a></h2>\n<p>Everything here depends on one number, and it is not the number the documentation gives.</p>\n<p>The LiteLLM proxy CLI has a <code>--num_workers</code> flag. The\n<a href=\"https://docs.litellm.ai/docs/proxy/cli\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">published reference</a> says its default is\n<em>\"Number of logical CPUs in the system, or 4 if that cannot be determined.\"</em> On an\neight-core box that would mean eight worker processes, eight event loops, and a blocking\ncall stalling one eighth of your traffic.</p>\n<p>Ask the host what is actually running — from outside the container, so nothing inside it\ncan misreport:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">top</span><span class=\"token plain\"> llm-gateway</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">UID    PID       PPID      C  STIME  TTY  TIME      CMD</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">root   2477458   2477435   3  16:43  ?    00:01:19  /app/.venv/bin/python3 /app/.venv/bin/litellm --config /app/config.yaml --port 4000</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">root   2481091   2477458   0  16:44  ?    00:00:05  /opt/prisma/binaries/node_modules/prisma/query-engine-debian-openssl-3.0.x -p 53053</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker top showing one litellm python process and one Prisma query engine\" src=\"https://development-wec.wiline.com/docs/assets/images/gw5-one-process-a28a66c6eafcb70fa88a41c347ddd22d.png\" width=\"643\" height=\"182\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 1.</strong> One python process runs the whole proxy. The second entry is Prisma, the\ndatabase client, which does not serve requests.</p>\n<p>If <code>--num_workers</code> were greater than one you would see several python processes with the\nfirst one as their parent. Confirm it in the startup log:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> logs llm-gateway </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-E</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Started server process|Uvicorn running\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-2</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INFO:     Started server process [1]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INFO:     Uvicorn running on http://0.0.0.0:4000 (Press CTRL+C to quit)</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The startup log showing a single server process and uvicorn bound to port 4000\" src=\"https://development-wec.wiline.com/docs/assets/images/gw5-startup-log-df1d1c66b00b54c6d3cca0e6787ae4eb.png\" width=\"645\" height=\"87\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 2.</strong> One server process. This log accumulates across restarts, so <code>tail -2</code>\nkeeps you looking at the current one.</p>\n<p>Now ask the tool what its default is:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway litellm </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--help</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-A5</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"^  --num_workers\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --num_workers INTEGER           Number of worker processes for uvicorn /</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                  gunicorn, or Granian worker processes</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                  (--workers). Default is 1 (from</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                  DEFAULT_NUM_WORKERS_LITELLM_PROXY). With</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                  --run_granian, use --granian_threads for</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                  runtime threads per worker.</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The litellm help output with the phrase Default is 1 highlighted\" src=\"https://development-wec.wiline.com/docs/assets/images/gw5-worker-default-a862f11836711c637727b9c24b5d3b0a.png\" width=\"922\" height=\"125\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 3.</strong> The tool's own help text: <em>\"Default is 1.\"</em></p>\n<p>That is a help string. Since we are about to contradict the published documentation, take\nit from the source too — adjust the python version in the path to match your image:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-rn</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'DEFAULT_NUM_WORKERS_LITELLM_PROXY'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  /app/.venv/lib/python3.13/site-packages/litellm/constants.py</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">15:DEFAULT_NUM_WORKERS_LITELLM_PROXY = int(os.getenv(\"DEFAULT_NUM_WORKERS_LITELLM_PROXY\", 1))</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>The published reference is wrong about this</div><div class=\"admonitionContent_BuS1\"><p>The <a href=\"https://docs.litellm.ai/docs/proxy/cli\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">CLI reference</a> states the <code>--num_workers</code>\ndefault as <em>\"Number of logical CPUs in the system, or 4 if that cannot be determined.\"</em>\nThree sources say otherwise: the running container, the installed <code>1.96.2</code> source, and\ncurrent upstream <code>main</code>, where both the constant and the help string still say <strong>1</strong>.</p><p>It is not a version skew, and it is not something in this deployment. Check yours:</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway </span><span class=\"token function\" style=\"color:#d73a49\">env</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> worker</span><br></div></code></pre></div></div><p>Empty output means you are on the default, which is one worker.</p></div></div>\n<p>So: one process, one event loop, shared by every request in flight. That is the condition\nthat turns a blocking call from a tax into an outage.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"a-load-script\">A load script<a href=\"https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/#a-load-script\" class=\"hash-link\" aria-label=\"Direct link to A load script\" title=\"Direct link to A load script\" translate=\"no\">​</a></h2>\n<p>Fire N requests at once, time each, report the wall clock for the batch. The message\ncarries PII so the masking callback has real work to do. A failed request is counted\nrather than allowed to abandon the run — you will need that.</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/llm-gateway/loadtest.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"Fire N chat completions at the gateway at once and report how long they take.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"display:inline-block;color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">The message carries PII so the Presidio masking callback has real work to do.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">A failed request is counted and reported, not allowed to abandon the batch.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> asyncio</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> statistics</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> sys</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> time</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> httpx</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">GATEWAY </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://127.0.0.1:4000/v1/chat/completions\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">KEY </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"LITELLM_MASTER_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MODEL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"qwen-small\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">PROMPT </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"Priya Raghunathan called from 415-555-0134 about her invoice. \"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"Reply with exactly: ok\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">one</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> i</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    start </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">perf_counter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">try</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">post</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            GATEWAY</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            headers</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Bearer </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">KEY</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            json</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:#e3116c\">\"model\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> MODEL</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"user\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> PROMPT</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:#e3116c\">\"max_tokens\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">5</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">except</span><span class=\"token plain\"> Exception </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> exc</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> i</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\">type</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">exc</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">__name__</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> i</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">status_code</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">perf_counter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token plain\"> start</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    n </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">int</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> httpx</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">AsyncClient</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">timeout</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">120.0</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        wall </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">perf_counter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        results </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">gather</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token operator\" style=\"color:#393A34\">*</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">one</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> i</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> i </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token builtin\">range</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">n</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        wall </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">perf_counter</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token plain\"> wall</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    times </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">sorted</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">e </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> _</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> _</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> e</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> _ </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> results </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> e </span><span class=\"token keyword\" style=\"color:#00009f\">is</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    errors </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">err </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> _</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> _</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> _</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> err </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> results </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> err</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"concurrency </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">n</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">  ok </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation builtin\">len</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">times</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">  failed </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation builtin\">len</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">errors</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> errors</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"  errors   </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">', '</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">join</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation builtin\">sorted</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation builtin\">set</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">errors</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"  wall     </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">wall</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">.2f</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">s\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> times</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"  fastest  </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">times</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation number\" style=\"color:#36acaa\">0</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">.2f</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">s\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"  median   </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">statistics</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">median</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">times</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">.2f</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">s\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"  slowest  </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">times</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation operator\" style=\"color:#393A34\">-</span><span class=\"token string-interpolation interpolation number\" style=\"color:#36acaa\">1</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">.2f</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">s\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">asyncio</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/llm-gateway </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-a</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">.</span><span class=\"token plain\"> ./.env </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python loadtest.py </span><span class=\"token number\" style=\"color:#36acaa\">8</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">concurrency 8  ok 8  failed 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  wall     1.93s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  fastest  1.60s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  median   1.91s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  slowest  1.93s</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Eight concurrent requests through the gateway, all succeeding, batch wall time 1.93 seconds\" src=\"https://development-wec.wiline.com/docs/assets/images/gw5-first-load-be7eeba783710fd2f310b7add1c21fdc.png\" width=\"645\" height=\"120\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 4.</strong> Eight at once, none failed. On its own this number means nothing yet.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"you-need-a-control-or-your-numbers-mean-nothing\">You need a control, or your numbers mean nothing<a href=\"https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/#you-need-a-control-or-your-numbers-mean-nothing\" class=\"hash-link\" aria-label=\"Direct link to You need a control, or your numbers mean nothing\" title=\"Direct link to You need a control, or your numbers mean nothing\" translate=\"no\">​</a></h2>\n<p>Here is the trap. The gateway calls a model over the network. That model has its own\nlatency, its own load, and its own bad afternoons. When a batch takes twelve seconds you\ncannot tell whether your gateway stalled or the upstream was busy — and if you guess, you\nwill publish nonsense.</p>\n<p>So measure the upstream directly, with the gateway out of the path. Copy the file and\nchange the three constants at the top:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/llm-gateway</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">cp</span><span class=\"token plain\"> loadtest.py loadtest_direct.py</span><br></div></code></pre></div></div>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/llm-gateway/loadtest_direct.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">GATEWAY </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"https://inference.wiline.com/v1/chat/completions\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">KEY </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"WEC_API_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MODEL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Qwen2.5-3B-Instruct\"</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python loadtest_direct.py </span><span class=\"token number\" style=\"color:#36acaa\">8</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">concurrency 8  ok 8  failed 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  wall     1.15s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  fastest  1.05s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  median   1.10s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  slowest  1.15s</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Eight concurrent requests straight to the upstream, batch wall time 1.15 seconds\" src=\"https://development-wec.wiline.com/docs/assets/images/gw5-control-6c9353892f98ded0047bab740eae67a0.png\" width=\"643\" height=\"126\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 5.</strong> The same eight requests with the gateway removed. This is the baseline\nevery gateway number gets read against.</p>\n<p>Now run the two <strong>interleaved</strong>, not one after the other, so a slow minute upstream\ncannot land entirely on one version:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/llm-gateway/run_experiment.sh</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token shebang important\">#!/bin/bash</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Interleave gateway runs with direct-to-upstream control runs so an upstream</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># slowdown cannot be mistaken for a gateway effect.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> -a</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">.</span><span class=\"token plain\"> ./.env</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">REPS</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">5</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># A restart takes longer than it looks. Firing requests at a port that is not</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># answering yet produces instant failures that have nothing to do with the callback.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">printf</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'waiting for the gateway'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">_</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">seq</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable number\" style=\"color:#36acaa\">1</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable number\" style=\"color:#36acaa\">60</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sf</span><span class=\"token plain\"> http://127.0.0.1:4000/health/liveliness </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">break</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">printf</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.'</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sf</span><span class=\"token plain\"> http://127.0.0.1:4000/health/liveliness </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">||</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\" never came up — aborting\"</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exit</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\" up\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Do not trust a label passed on the command line — ask the gateway which callback</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># it actually loaded. A mislabelled run is worse than no run.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">LABEL</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">docker</span><span class=\"token variable\" style=\"color:#36acaa\"> logs llm-gateway </span><span class=\"token variable operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token variable operator\" style=\"color:#393A34\">&gt;</span><span class=\"token variable file-descriptor important\" style=\"color:#36acaa\">&amp;1</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">grep</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'\\[scrubber\\]'</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">tail</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-1</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">sed</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'s/.*loaded: //'</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-z</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$LABEL</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">then</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"cannot determine loaded callback — aborting\"</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exit</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">fi</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"callback in use: </span><span class=\"token string variable\" style=\"color:#36acaa\">$LABEL</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function-name function\" style=\"color:#d73a49\">summarise</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">awk</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'/^concurrency/{failed=$6} /wall/{wall=$2} END{printf \"%s\", wall; if (failed+0 &gt; 0) printf \"(%d failed)\", failed}'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># The first request after a restart pays for imports and connection pools, which</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># has nothing to do with the callback. Warm both paths before recording anything.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python loadtest.py </span><span class=\"token number\" style=\"color:#36acaa\">4</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python loadtest_direct.py </span><span class=\"token number\" style=\"color:#36acaa\">4</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">n</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">8</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">16</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">i</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">seq</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable number\" style=\"color:#36acaa\">1</span><span class=\"token variable\" style=\"color:#36acaa\"> $REPS</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">g</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/.venv/bin/python loadtest.py $n </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> summarise</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">d</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">~/.venv/bin/python loadtest_direct.py $n </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> summarise</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$LABEL</span><span class=\"token string\" style=\"color:#e3116c\"> N=</span><span class=\"token string variable\" style=\"color:#36acaa\">$n</span><span class=\"token string\" style=\"color:#e3116c\"> rep=</span><span class=\"token string variable\" style=\"color:#36acaa\">$i</span><span class=\"token string\" style=\"color:#e3116c\"> gateway=</span><span class=\"token string variable\" style=\"color:#36acaa\">$g</span><span class=\"token string\" style=\"color:#e3116c\"> direct=</span><span class=\"token string variable\" style=\"color:#36acaa\">$d</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">done</span><br></div></code></pre></div></div>\n<p>Three details in there are not decoration, and each one cost a wasted run to learn:</p>\n<p><strong>It waits for the gateway.</strong> A <code>docker compose restart</code> can take longer than ten\nseconds, and firing at a port that is not listening yet produces instant failures —\n<code>gateway=0.03s(8 failed)</code> — that look like a catastrophic result and mean nothing.</p>\n<p><strong>It refuses to take a label from you.</strong> It reads the loaded callback out of the\ngateway's log. Pass <code>blocking</code> on the command line while the async callback is loaded and\nyou will measure the same code twice, see no difference, and conclude there is nothing\nhere. That happened twice while writing this.</p>\n<p><strong>It throws away a warm-up pass.</strong> The first batch after a restart pays for imports and\nconnection pools, and comes in three to six times slower than the next one regardless of\nwhich callback is loaded.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"make-each-version-announce-itself\">Make each version announce itself<a href=\"https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/#make-each-version-announce-itself\" class=\"hash-link\" aria-label=\"Direct link to Make each version announce itself\" title=\"Direct link to Make each version announce itself\" translate=\"no\">​</a></h2>\n<p>You are about to compare two versions of one file, and you need certainty about which is\nlive. LiteLLM's startup log lists success and failure callbacks but not\n<code>litellm_settings.callbacks</code>, and there is no endpoint that reports them —\n<code>/get/config/callbacks</code> answers <code>200</code> with <code>{\"detail\":\"Not Found\"}</code>.</p>\n<p>So have each version say its own name at import, where it costs nothing per request:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># last line of scrubber.py</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"[scrubber] loaded: async httpx.AsyncClient\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># last line of scrubber_blocking.py</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"[scrubber] loaded: blocking httpx.Client\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Mount both files, and switch with one line of config:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.yml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">volumes</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./config.yaml</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/config.yaml</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./scrubber.py</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/scrubber.py</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./scrubber_blocking.py</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/scrubber_blocking.py</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./recognizers.json</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/recognizers.json</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div></code></pre></div></div>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_settings</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">callbacks</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scrubber.instance\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\">          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># or scrubber_blocking.instance</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/scrubber.instance/scrubber_blocking.instance/'</span><span class=\"token plain\"> config.yaml</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose restart litellm</span><br></div></code></pre></div></div>\n<p>The experiment script prints the loaded callback as its first line, so every run carries\nits own proof of what it measured.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-numbers\">The numbers<a href=\"https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/#the-numbers\" class=\"hash-link\" aria-label=\"Direct link to The numbers\" title=\"Direct link to The numbers\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">./run_experiment.sh</span><br></div></code></pre></div></div>\n<p>With the async client:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">callback in use: async httpx.AsyncClient</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=8 rep=1 gateway=0.98s direct=1.09s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=8 rep=2 gateway=0.86s direct=0.80s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=8 rep=3 gateway=0.92s direct=0.96s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=8 rep=4 gateway=0.78s direct=0.85s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=8 rep=5 gateway=0.90s direct=0.82s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=16 rep=1 gateway=1.61s direct=1.49s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=16 rep=2 gateway=1.65s direct=1.26s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=16 rep=3 gateway=1.28s direct=1.35s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=16 rep=4 gateway=1.33s direct=1.42s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">async httpx.AsyncClient N=16 rep=5 gateway=1.77s direct=1.56s</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Ten interleaved runs with the async callback, gateway times tracking the control on every row\" src=\"https://development-wec.wiline.com/docs/assets/images/gw5-async-runs-44f396a0682d60c0748426e50abc69cf.png\" width=\"508\" height=\"233\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 6.</strong> The async client. Every gateway number sits beside its control, at both\nconcurrency levels. Nothing stands out because nothing happened.</p>\n<p>Then swap the callback and run the same thing:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">waiting for the gateway up</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">callback in use: blocking httpx.Client</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=8 rep=1 gateway=1.20s direct=0.93s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=8 rep=2 gateway=0.96s direct=0.83s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=8 rep=3 gateway=0.92s direct=0.86s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=8 rep=4 gateway=0.73s direct=0.90s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=8 rep=5 gateway=1.70s direct=0.90s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=16 rep=1 gateway=70.18s direct=2.00s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=16 rep=2 gateway=1.75s direct=1.70s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=16 rep=3 gateway=2.33s direct=1.78s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=16 rep=4 gateway=2.88s direct=1.49s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocking httpx.Client N=16 rep=5 gateway=5.14s direct=1.19s</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Ten interleaved runs with the blocking callback, one batch at 70.18 seconds and another at 5.14 against controls near 1.2 to 2 seconds\" src=\"https://development-wec.wiline.com/docs/assets/images/gw5-blocking-runs-df9535bdd0ed417cb65599e60ca5e742.png\" width=\"512\" height=\"239\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 7.</strong> The blocking client. Same script, same load, one word different in the\ncode. The control column holds at 1.19-2.00s throughout.</p>\n<table><thead><tr><th>Version</th><th>N</th><th>Gateway median</th><th>Gateway worst</th><th>Control median</th></tr></thead><tbody><tr><td>async <code>AsyncClient</code></td><td>8</td><td>0.90s</td><td>0.98s</td><td>0.85s</td></tr><tr><td>async <code>AsyncClient</code></td><td>16</td><td>1.61s</td><td>1.77s</td><td>1.42s</td></tr><tr><td>blocking <code>Client</code></td><td>8</td><td>0.96s</td><td>1.70s</td><td>0.90s</td></tr><tr><td>blocking <code>Client</code></td><td>16</td><td>2.88s</td><td><strong>70.18s</strong> *</td><td>1.70s</td></tr></tbody></table>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Log-scale dot plot of every batch in both runs. Grey control dots cluster between 0.7 and 2 seconds in all four groups. Green async dots sit among them. Two red blocking dots break away at 5.14 and 70.18 seconds\" src=\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3NjAgNDQwIiB3aWR0aD0iNzYwIiBoZWlnaHQ9IjQ0MCIgZm9udC1mYW1pbHk9InN5c3RlbS11aSwtYXBwbGUtc3lzdGVtLFNlZ29lIFVJLHNhbnMtc2VyaWYiPgo8dGl0bGU+V2FsbCB0aW1lIHBlciBiYXRjaCwgdGhyb3VnaCB0aGUgZ2F0ZXdheSB2ZXJzdXMgc3RyYWlnaHQgdG8gdGhlIHVwc3RyZWFtPC90aXRsZT4KPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9Ijc2MCIgaGVpZ2h0PSI0NDAiIGZpbGw9IiNmZmZmZmYiLz4KPGxpbmUgeDE9Ijg2IiB5MT0iMzUyLjAiIHgyPSI3MzAiIHkyPSIzNTIuMCIgc3Ryb2tlPSIjOTRhM2I4IiBzdHJva2Utb3BhY2l0eT0iMC4zMCIvPgo8dGV4dCB4PSI3NCIgeT0iMzU2LjAiIHRleHQtYW5jaG9yPSJlbmQiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiM2NDc0OGIiPjAuNXM8L3RleHQ+CjxsaW5lIHgxPSI4NiIgeTE9IjMxMS43IiB4Mj0iNzMwIiB5Mj0iMzExLjciIHN0cm9rZT0iIzk0YTNiOCIgc3Ryb2tlLW9wYWNpdHk9IjAuMzAiLz4KPHRleHQgeD0iNzQiIHk9IjMxNS43IiB0ZXh0LWFuY2hvcj0iZW5kIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj4xczwvdGV4dD4KPGxpbmUgeDE9Ijg2IiB5MT0iMjcxLjQiIHgyPSI3MzAiIHkyPSIyNzEuNCIgc3Ryb2tlPSIjOTRhM2I4IiBzdHJva2Utb3BhY2l0eT0iMC4zMCIvPgo8dGV4dCB4PSI3NCIgeT0iMjc1LjQiIHRleHQtYW5jaG9yPSJlbmQiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiM2NDc0OGIiPjJzPC90ZXh0Pgo8bGluZSB4MT0iODYiIHkxPSIyMTguMSIgeDI9IjczMCIgeTI9IjIxOC4xIiBzdHJva2U9IiM5NGEzYjgiIHN0cm9rZS1vcGFjaXR5PSIwLjMwIi8+Cjx0ZXh0IHg9Ijc0IiB5PSIyMjIuMSIgdGV4dC1hbmNob3I9ImVuZCIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzY0NzQ4YiI+NXM8L3RleHQ+CjxsaW5lIHgxPSI4NiIgeTE9IjE3Ny45IiB4Mj0iNzMwIiB5Mj0iMTc3LjkiIHN0cm9rZT0iIzk0YTNiOCIgc3Ryb2tlLW9wYWNpdHk9IjAuMzAiLz4KPHRleHQgeD0iNzQiIHk9IjE4MS45IiB0ZXh0LWFuY2hvcj0iZW5kIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj4xMHM8L3RleHQ+CjxsaW5lIHgxPSI4NiIgeTE9IjEzNy42IiB4Mj0iNzMwIiB5Mj0iMTM3LjYiIHN0cm9rZT0iIzk0YTNiOCIgc3Ryb2tlLW9wYWNpdHk9IjAuMzAiLz4KPHRleHQgeD0iNzQiIHk9IjE0MS42IiB0ZXh0LWFuY2hvcj0iZW5kIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj4yMHM8L3RleHQ+CjxsaW5lIHgxPSI4NiIgeTE9Ijg0LjMiIHgyPSI3MzAiIHkyPSI4NC4zIiBzdHJva2U9IiM5NGEzYjgiIHN0cm9rZS1vcGFjaXR5PSIwLjMwIi8+Cjx0ZXh0IHg9Ijc0IiB5PSI4OC4zIiB0ZXh0LWFuY2hvcj0iZW5kIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj41MHM8L3RleHQ+CjxsaW5lIHgxPSI4NiIgeTE9IjQ0LjAiIHgyPSI3MzAiIHkyPSI0NC4wIiBzdHJva2U9IiM5NGEzYjgiIHN0cm9rZS1vcGFjaXR5PSIwLjMwIi8+Cjx0ZXh0IHg9Ijc0IiB5PSI0OC4wIiB0ZXh0LWFuY2hvcj0iZW5kIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj4xMDBzPC90ZXh0Pgo8Y2lyY2xlIGN4PSIxNDAuNSIgY3k9IjMwNi43IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIxNDAuNSIgY3k9IjMyNC43IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIxNDAuNSIgY3k9IjMxNC4xIiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIxNDAuNSIgY3k9IjMyMS4yIiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIxNDAuNSIgY3k9IjMyMy4yIiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIxOTIuNSIgY3k9IjMxMi45IiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSIxOTIuNSIgY3k9IjMyMC41IiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSIxOTIuNSIgY3k9IjMxNi42IiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSIxOTIuNSIgY3k9IjMyNi4xIiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSIxOTIuNSIgY3k9IjMxNy44IiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8dGV4dCB4PSIxNjYuNSIgeT0iMzc2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj5hc3luYzwvdGV4dD4KPHRleHQgeD0iMTY2LjUiIHk9IjM5MyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzY0NzQ4YiI+Tj04PC90ZXh0Pgo8Y2lyY2xlIGN4PSIzMDEuNSIgY3k9IjMxNS45IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIzMDEuNSIgY3k9IjMyMi41IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIzMDEuNSIgY3k9IjMyMC41IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIzMDEuNSIgY3k9IjMxNy44IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIzMDEuNSIgY3k9IjMxNy44IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSIzNTMuNSIgY3k9IjMwMS4xIiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSIzNTMuNSIgY3k9IjMxNC4xIiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSIzNTMuNSIgY3k9IjMxNi42IiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSIzNTMuNSIgY3k9IjMzMC4wIiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSIzNTMuNSIgY3k9IjI4MC45IiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8dGV4dCB4PSIzMjcuNSIgeT0iMzc2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj5ibG9ja2luZzwvdGV4dD4KPHRleHQgeD0iMzI3LjUiIHk9IjM5MyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzY0NzQ4YiI+Tj04PC90ZXh0Pgo8Y2lyY2xlIGN4PSI0NjIuNSIgY3k9IjI4OC41IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSI0NjIuNSIgY3k9IjI5OC4zIiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSI0NjIuNSIgY3k9IjI5NC4zIiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSI0NjIuNSIgY3k9IjI5MS4zIiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSI0NjIuNSIgY3k9IjI4NS45IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiIGZpbGwtb3BhY2l0eT0iMC44NSIvPgo8Y2lyY2xlIGN4PSI1MTQuNSIgY3k9IjI4NC4wIiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSI1MTQuNSIgY3k9IjI4Mi42IiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSI1MTQuNSIgY3k9IjI5Ny40IiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSI1MTQuNSIgY3k9IjI5NS4xIiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSI1MTQuNSIgY3k9IjI3OC41IiByPSI1IiBmaWxsPSIjMDU5NjY5IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8dGV4dCB4PSI0ODguNSIgeT0iMzc2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj5hc3luYzwvdGV4dD4KPHRleHQgeD0iNDg4LjUiIHk9IjM5MyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzY0NzQ4YiI+Tj0xNjwvdGV4dD4KPGNpcmNsZSBjeD0iNjIzLjUiIGN5PSIyNzEuNCIgcj0iNC41IiBmaWxsPSIjOTRhM2I4IiBmaWxsLW9wYWNpdHk9IjAuODUiLz4KPGNpcmNsZSBjeD0iNjIzLjUiIGN5PSIyODAuOSIgcj0iNC41IiBmaWxsPSIjOTRhM2I4IiBmaWxsLW9wYWNpdHk9IjAuODUiLz4KPGNpcmNsZSBjeD0iNjIzLjUiIGN5PSIyNzguMiIgcj0iNC41IiBmaWxsPSIjOTRhM2I4IiBmaWxsLW9wYWNpdHk9IjAuODUiLz4KPGNpcmNsZSBjeD0iNjIzLjUiIGN5PSIyODguNSIgcj0iNC41IiBmaWxsPSIjOTRhM2I4IiBmaWxsLW9wYWNpdHk9IjAuODUiLz4KPGNpcmNsZSBjeD0iNjIzLjUiIGN5PSIzMDEuNiIgcj0iNC41IiBmaWxsPSIjOTRhM2I4IiBmaWxsLW9wYWNpdHk9IjAuODUiLz4KPGNpcmNsZSBjeD0iNjc1LjUiIGN5PSI2NC42IiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSI2NzUuNSIgY3k9IjI3OS4yIiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSI2NzUuNSIgY3k9IjI2Mi41IiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSI2NzUuNSIgY3k9IjI1MC4yIiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8Y2lyY2xlIGN4PSI2NzUuNSIgY3k9IjIxNi41IiByPSI1IiBmaWxsPSIjZGMyNjI2IiBmaWxsLW9wYWNpdHk9IjAuOSIvPgo8dGV4dCB4PSI2NDkuNSIgeT0iMzc2IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmb250LXNpemU9IjEzIiBmaWxsPSIjNjQ3NDhiIj5ibG9ja2luZzwvdGV4dD4KPHRleHQgeD0iNjQ5LjUiIHk9IjM5MyIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzY0NzQ4YiI+Tj0xNjwvdGV4dD4KPHRleHQgeD0iNjg3LjUiIHk9IjY4LjYiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiNkYzI2MjYiPjcwLjE4cyAqPC90ZXh0Pgo8dGV4dCB4PSI2ODcuNSIgeT0iMjIwLjUiIGZvbnQtc2l6ZT0iMTMiIGZvbnQtd2VpZ2h0PSI2MDAiIGZpbGw9IiNkYzI2MjYiPjUuMTRzPC90ZXh0Pgo8Y2lyY2xlIGN4PSI5NCIgY3k9IjI4IiByPSI0LjUiIGZpbGw9IiM5NGEzYjgiLz4KPHRleHQgeD0iMTA2IiB5PSIzMiIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzY0NzQ4YiI+c3RyYWlnaHQgdG8gdGhlIHVwc3RyZWFtIChjb250cm9sKTwvdGV4dD4KPGNpcmNsZSBjeD0iMzg2IiBjeT0iMjgiIHI9IjUiIGZpbGw9IiMwNTk2NjkiLz4KPHRleHQgeD0iMzk4IiB5PSIzMiIgZm9udC1zaXplPSIxMyIgZmlsbD0iIzY0NzQ4YiI+Z2F0ZXdheSwgYXN5bmM8L3RleHQ+CjxjaXJjbGUgY3g9IjUzNiIgY3k9IjI4IiByPSI1IiBmaWxsPSIjZGMyNjI2Ii8+Cjx0ZXh0IHg9IjU0OCIgeT0iMzIiIGZvbnQtc2l6ZT0iMTMiIGZpbGw9IiM2NDc0OGIiPmdhdGV3YXksIGJsb2NraW5nPC90ZXh0Pgo8dGV4dCB4PSI4NiIgeT0iNDI4IiBmb250LXNpemU9IjEyIiBmaWxsPSIjNjQ3NDhiIj4qIHRoaXMgYmF0Y2ggb3ZlcmxhcHBlZCBhbiB1bnJlbGF0ZWQgbG9hZCB0ZXN0IG9uIHRoZSBzYW1lIGhvc3Qg4oCUIHRyZWF0IGl0IGFzIGFuIHVwcGVyIGJvdW5kPC90ZXh0Pgo8L3N2Zz4=\" width=\"760\" height=\"440\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 8.</strong> Every batch from Figures 6 and 7 on a log axis. The control stays inside a\nnarrow band in all four groups. The async runs stay with it. Two blocking runs leave it.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>One number in that table needs an asterisk</div><div class=\"admonitionContent_BuS1\"><p>The 70.18s batch overlapped an unrelated load test running against the same gateway, so\npart of it is not attributable to the callback. It is reported because it happened, not\nbecause it is clean. Read the 5.14s batch as the representative stall — still more than\nfour times its own control of 1.19s.</p><p>The effect itself reproduced across four separate runs on this host, with N=16 stalls of\n11.22s, 12.44s, 21.22s, 31.14s and 5.14s. Across every clean async run: none.</p></div></div>\n<p>Read the async rows first. At N=8 the gateway's median is 0.90s and the control's is\n0.85s. The callback, Presidio round trips and all, disappears into the model's own\nlatency.</p>\n<p>Now the blocking row at N=8. Median 0.96s against a 0.90s control. That is 0.06s. If you\nmeasured medians and shipped, you would call it fine — and the worst batch in the same\nfive took 1.70s while its own control took 0.90s.</p>\n<p>At N=16 it stops hiding. The median goes to 2.88s against 1.70s, and the tail runs off\nthe chart.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can tell whether a slow gateway is your own code or the model it calls — run the same\nload against both, interleaved, and read the tail instead of the median.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"it-does-not-just-get-slow-it-drops-requests\">It does not just get slow. It drops requests.<a href=\"https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/#it-does-not-just-get-slow-it-drops-requests\" class=\"hash-link\" aria-label=\"Direct link to It does not just get slow. It drops requests.\" title=\"Direct link to It does not just get slow. It drops requests.\" translate=\"no\">​</a></h2>\n<p>Before failure counting was added to the load script, a blocking run died outright:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">httpx.RemoteProtocolError: Server disconnected without sending a response.</span><br></div></code></pre></div></div>\n<p>Two of ten batches in that run lost requests that way. Zero async batches ever did. The\ngateway was not slow for those callers. It hung up on them.</p>\n<p>Ask the gateway what it thinks happened:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> logs llm-gateway </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"httpcore/_sync\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> logs llm-gateway </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-A1</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"httpx.ReadTimeout\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">tail</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-4</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">77</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INFO:     172.22.0.1:47566 - \"POST /v1/chat/completions HTTP/1.1\" 200 OK</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">--</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">httpx.ReadTimeout: timed out</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INFO:     172.22.0.1:52870 - \"POST /v1/chat/completions HTTP/1.1\" 200 OK</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Seventy-seven sync stack frames in the log, and a ReadTimeout sitting between two successful 200 OK responses\" src=\"https://development-wec.wiline.com/docs/assets/images/gw5-sync-timeout-d454e62c9e5512f71923e0ec2c0efbec.png\" width=\"645\" height=\"142\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 9.</strong> <code>httpcore/_sync</code> appears 77 times. The async client would produce\n<code>_async</code>. And the timeout sits between two <code>200 OK</code> lines.</p>\n<p>That <code>_sync</code> is the fingerprint. It proves the failing call is the blocking Presidio call\nand not something else in the stack. The full error names the caller:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">LiteLLM:ERROR: logging_worker.py:103 - LoggingWorker error: timed out</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  File \".../httpcore/_sync/connection_pool.py\", line 236, in handle_request</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  httpcore.ReadTimeout: timed out</span><br></div></code></pre></div></div>\n<p><strong>Now look at what surrounds it.</strong> <code>200 OK</code>. The requests succeeded. The masking is what\nfailed.</p>\n<p>That is the part worth stopping on, because Part 4's whole promise was that PII reaches\nyour traces already masked. Under load, with the blocking client, the masking call times\nout against its own ten-second limit while the caller receives a normal success. Nothing\nin the response tells you the guarantee lapsed. You would have to be reading the\ngateway's stderr to know.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-is-actually-happening\">What is actually happening<a href=\"https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/#what-is-actually-happening\" class=\"hash-link\" aria-label=\"Direct link to What is actually happening\" title=\"Direct link to What is actually happening\" translate=\"no\">​</a></h2>\n<p><code>httpx.Client</code> inside an <code>async def</code> does not yield. When the callback calls Presidio, the\nthread running the event loop sits in a socket read until Presidio answers, and during\nthat time the loop services nothing — not another request's model call, not a response\ncoming back. It is one loop, as you confirmed at the start.</p>\n<p>Each request triggers several of these calls: the messages, the copy in the standard\nlogging object, and the response. So under concurrency the requests do not overlap. They\nqueue, and each one's wait is every earlier one's Presidio time added together. That is\nwhy the effect is not a constant tax — it depends on how many requests happen to arrive\nwhile the loop is held, which is also why the numbers are spiky rather than uniformly\nworse.</p>\n<p>Past a certain queue depth the waits exceed the scrubber's own <code>timeout=10.0</code> and the\nmasking gives up, while client connections waiting on a frozen loop get dropped. The\nmedian hides all of it because most batches get lucky. The tail is the truth.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"fixing-it\">Fixing it<a href=\"https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/#fixing-it\" class=\"hash-link\" aria-label=\"Direct link to Fixing it\" title=\"Direct link to Fixing it\" translate=\"no\">​</a></h2>\n<p>The fix is the one Part 4 landed on, and this is the evidence for it: <code>httpx.AsyncClient</code>\nwith <code>await</code>, which yields the loop while Presidio works.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">diff</span><span class=\"token plain\"> scrubber.py scrubber_blocking.py</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">26,27c26,27</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&lt;     async with httpx.AsyncClient(timeout=10.0) as client:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&lt;         r = await client.post(f\"{ANALYZER}/analyze\", json={\"text\": text, \"language\": \"en\"})</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">---</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&gt;     with httpx.Client(timeout=10.0) as client:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&gt;         r = client.post(f\"{ANALYZER}/analyze\", json={\"text\": text, \"language\": \"en\"})</span><br></div></code></pre></div></div>\n<p>Two words and an <code>await</code>. That is the whole difference between Figure 6 and Figure 7.</p>\n<p>More workers is not the fix, and the documentation explains why better than we could. On\n<code>--timeout_worker_healthcheck</code>, describing <code>--num_workers &gt; 1</code>:</p>\n<blockquote>\n<p><em>\"the supervisor process pings each worker; a worker that does not respond within this\nwindow (for example because its event loop is blocked by synchronous work) is killed\nwith SIGKILL and replaced.\"</em></p>\n</blockquote>\n<p>At one worker there is no supervisor, so a blocked loop stalls. At several, a blocked\nworker gets killed and everything in flight on it dies instead of waiting. Neither is a\nfix for synchronous work on an event loop. Fix the call.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Prove it holds under load</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-we-did-not-test\">What we did not test<a href=\"https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/#what-we-did-not-test\" class=\"hash-link\" aria-label=\"Direct link to What we did not test\" title=\"Direct link to What we did not test\" translate=\"no\">​</a></h2>\n<p>Five reps per cell is enough to show that a stall exists next to a steady control. It is\nnot enough to characterise the distribution — how often, how bad, or how it scales past\nN=16. If you run this in production, run it longer and look at percentiles.</p>\n<p>We did not test <code>--num_workers &gt; 1</code>, streaming responses, or a slow or remote Presidio\nrather than a healthy local container.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-the-timeout-actually-costs-you\">What the timeout actually costs you<a href=\"https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/#what-the-timeout-actually-costs-you\" class=\"hash-link\" aria-label=\"Direct link to What the timeout actually costs you\" title=\"Direct link to What the timeout actually costs you\" translate=\"no\">​</a></h2>\n<p>The obvious worry, when the masking call times out on a request that returned <code>200 OK</code>,\nis that the unmasked payload lands in the trace anyway. It does not.</p>\n<p>Sustained load — six rounds of twenty-four concurrent requests, each carrying a name and\na phone number — produced <strong>77 masking timeouts</strong> in the gateway log. Of the 144\nrequests, <strong>62 traces reached Langfuse and 82 never arrived at all.</strong> Every one of the 62\nwas properly masked. None carried a raw name.</p>\n<p>So the failure is not a privacy failure. It is an observability failure, and a quiet one:\nunder sustained load more than half the traffic simply is not recorded, while every\ncaller gets a normal success. If you are reading trace volume as a proxy for traffic, or\ncounting on traces for an audit trail, that gap is the thing to watch — and it is\ninvisible from the response side.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Measure detection before you measure anything else</div><div class=\"admonitionContent_BuS1\"><p>Building this test, requests were tagged with a short marker so each trace could be found\n— <code>[TAG-07] Priya Raghunathan called from …</code>. Nine traces then came back with the phone\nmasked and the name in the clear, which reads exactly like a load-induced leak.</p><p>It was not. Sequentially, with no load at all, that sentence sent straight to Presidio\nreturns only <code>PHONE_NUMBER</code>; the same sentence without the bracketed prefix returns\n<code>PERSON</code> and <code>PHONE_NUMBER</code> both. The marker suppressed name detection, and the callback\nfaithfully masked everything Presidio reported.</p><p>Check what your analyzer detects in your exact strings before concluding anything about\nmasking under load. An instrument that changes the thing it measures will hand you a\nfinding that is not there.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/\">Part 4 — clean traces, untouched answers</a> — the callback measured here</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/\">Part 3 — mask PII at the gateway</a> — the Presidio setup</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/\">Observe production with Langfuse</a> — where these traces land</li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/",
            "title": "Load test an LLM gateway and find the stall the median hides",
            "summary": "Part 4 measured a blocking call in a callback at 200-350ms and said the real damage only shows under concurrency. This runs it: sixteen requests at once, with a control against the upstream so you can tell your gateway's fault from the model's. The medians of the two versions are almost identical. One version drops requests and silently stops masking.",
            "date_modified": "2026-08-27T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "llm",
                "gateway",
                "litellm",
                "performance",
                "concurrency",
                "presidio",
                "observability",
                "wec",
                "wec-inference"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/",
            "content_html": "<div class=\"tutorialHero\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"487\" height=\"249\" fill=\"none\" viewBox=\"0 0 487 249\" class=\"tutorialHero__brand\"><path fill=\"#1C3C3C\" fill-rule=\"evenodd\" d=\"M124.273.412h239.031c68.111 0 123.52 55.603 123.52 123.948s-55.409 123.948-123.52 123.948H124.273c-68.11 0-123.52-55.603-123.52-123.948S56.164.412 124.274.412M234.018 192.55c3.001 3.156 7.443 3 11.378 2.182l.039.02c1.827-1.486-.77-3.368-3.249-5.165-1.487-1.077-2.932-2.125-3.356-3.038 1.373-1.674-2.687-5.474-5.847-8.433-1.327-1.242-2.495-2.335-3.037-3.061-2.248-2.448-3.152-5.537-4.062-8.644-.603-2.061-1.208-4.131-2.211-6.027-6.176-14.339-13.248-28.561-23.165-40.699-6.373-8.067-13.646-15.291-20.922-22.518-4.691-4.66-9.382-9.319-13.835-14.206-4.581-4.73-7.338-10.556-10.1-16.392-2.312-4.886-4.627-9.779-8.018-14.04-10.268-15.196-42.686-19.346-47.44 2.124.019.662-.195 1.09-.78 1.52-2.63 1.928-4.968 4.11-6.935 6.76-4.812 6.721-5.553 18.118.448 24.158l.025-.388c.2-3.05.388-5.9 2.8-8.087 4.637 3.994 11.67 5.416 17.047 2.435 6.483 9.3 8.548 20.543 10.621 31.823 1.726 9.398 3.457 18.821 7.751 27.171l.266.443c2.524 4.202 5.089 8.472 8.326 12.142 1.176 1.823 3.59 3.791 6 5.755 3.18 2.591 6.353 5.177 6.663 7.416.015.974.01 1.961.006 2.954-.025 5.879-.051 11.967 3.716 16.801 2.084 4.228-3.02 8.475-7.131 7.949-2.254.312-4.716-.282-7.161-.871-3.346-.807-6.659-1.607-9.36-.064-.758.82-1.846.849-2.94.877-1.297.035-2.601.069-3.373 1.422-.158.402-.528.856-.913 1.328-.846 1.036-1.762 2.159-.665 3.016q.149-.111.294-.224c1.663-1.269 3.248-2.479 5.493-1.724-.299 1.658.772 2.103 1.843 2.547q.281.114.553.239c-.011.385-.087.774-.163 1.159-.18.921-.356 1.824.358 2.62.339-.344.639-.732.939-1.12.735-.95 1.474-1.904 2.802-2.25 2.92 3.9 5.862 2.28 9.554.248 4.164-2.293 9.281-5.111 16.396-1.125-2.727-.136-5.163.195-6.994 2.455-.448.507-.838 1.091-.039 1.754 4.21-2.728 5.961-1.748 7.61-.825 1.19.666 2.326 1.302 4.294.493.465-.242.93-.493 1.396-.745 3.161-1.705 6.366-3.434 10.118-2.839-2.803.808-3.8 2.584-4.888 4.524-.539.959-1.099 1.957-1.911 2.898-.429.429-.624.936-.137 1.656 5.869-.488 8.087-1.976 11.083-3.986 1.429-.959 3.036-2.038 5.302-3.183 2.505-1.542 5.009-.556 7.436.4 2.633 1.037 5.175 2.037 7.527-.264.743-.7 1.674-.708 2.602-.717a10 10 0 0 0 1.002-.043c-.732-3.92-4.861-3.874-9.052-3.827-4.847.054-9.778.109-9.632-5.972 4.504-3.076 4.546-8.415 4.585-13.461.01-1.218.019-2.419.091-3.567 3.313 1.847 6.817 3.291 10.299 4.725 3.276 1.35 6.533 2.692 9.593 4.354 3.195 5.143 8.182 11.962 14.826 11.514.175-.526.331-.974.526-1.5.383.067.787.169 1.199.273 1.743.442 3.611.915 4.509-1.15m130.213-58.45a20.54 20.54 0 0 0 14.504 5.994c5.44 0 10.658-2.156 14.505-5.994a20.44 20.44 0 0 0 6.007-14.469 20.44 20.44 0 0 0-6.007-14.469 20.54 20.54 0 0 0-21.882-4.624l-11.757-17.162-8.194 5.614 11.818 17.25a20.43 20.43 0 0 0-5.002 13.391 20.43 20.43 0 0 0 6.008 14.469m-36.808-55.576a20.55 20.55 0 0 0 21.408-1.964 20.46 20.46 0 0 0 7.34-10.483 20.4 20.4 0 0 0-.331-12.784 20.47 20.47 0 0 0-7.873-10.09 20.557 20.557 0 0 0-18.35-2.282 20.5 20.5 0 0 0-7.964 5.175 20.44 20.44 0 0 0-4.77 8.201 20.429 20.429 0 0 0 3.232 18.164 20.5 20.5 0 0 0 7.308 6.063m0 118.824a20.55 20.55 0 0 0 21.408-1.964 20.46 20.46 0 0 0 7.34-10.483 20.4 20.4 0 0 0-.331-12.783 20.47 20.47 0 0 0-7.873-10.092 20.55 20.55 0 0 0-26.314 2.894 20.45 20.45 0 0 0-4.77 8.201 20.43 20.43 0 0 0 3.232 18.164 20.5 20.5 0 0 0 7.308 6.063m18.857-72.629v-10.174h-31.394a20.3 20.3 0 0 0-4.398-8.342L322.3 88.704l-8.59-5.698-11.812 17.499a20.5 20.5 0 0 0-6.749-1.221 20.5 20.5 0 0 0-14.462 5.959 20.3 20.3 0 0 0-5.99 14.388 20.3 20.3 0 0 0 5.99 14.387 20.5 20.5 0 0 0 14.462 5.96 20.5 20.5 0 0 0 6.749-1.221l11.812 17.498 8.487-5.697-11.709-17.498a20.3 20.3 0 0 0 4.398-8.342z\" clip-rule=\"evenodd\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/Postgresql_elephant.svg-62fedb3464ae364ae4e342c86b474ed6.png\" alt=\"PostgreSQL\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2255\" height=\"527\" fill=\"none\" viewBox=\"0 0 2255 527\" class=\"tutorialHero__langfuse\"><path fill=\"#1B1917\" d=\"M652.669 116.433c0-10.261-7.683-17.956-17.926-17.956H607V60h87.923v316.366h-42.254zM804.056 379.786c-39.693 0-72.131-27.362-72.131-68.831 0-41.042 28.596-69.686 84.935-69.686h39.693c7.256 0 12.805-5.558 12.805-12.826v-8.55c0-25.651-20.487-38.905-40.547-38.905-18.353 0-33.291 8.123-41.828 26.934h-45.668c11.95-44.462 46.095-65.41 88.349-65.41 40.547 0 82.374 22.231 82.374 77.808v156.046h-42.68v-14.109c0-5.13-5.549-7.695-9.817-4.702-16.646 11.97-32.438 22.231-55.485 22.231m5.975-38.477c18.78 0 34.572-9.406 52.498-27.362 4.695-4.702 6.829-10.688 6.829-17.1v-6.413c0-7.268-5.549-12.826-12.805-12.826H815.58c-27.743 0-40.974 12.398-40.974 31.637 0 17.956 12.377 32.064 35.425 32.064M961.855 376.366V147.642h42.255v19.666c0 5.13 6.4 6.84 10.24 2.992 13.23-12.825 31.59-27.788 60.61-27.788 36.71 0 70.85 23.086 70.85 75.671v158.183h-42.25V227.161c0-29.499-17.93-45.745-39.7-45.745-20.91 0-35 11.971-49.93 29.499-7.26 8.978-9.82 19.238-9.82 30.354v135.097zM1287.48 467c-52.5 0-85.79-24.796-95.61-63.273h46.1c7.25 15.818 20.06 25.651 45.67 25.651 34.14 0 55.48-20.093 55.48-65.838v-7.268c0-5.13-4.27-7.695-9.39-3.42-14.08 12.398-32.01 20.093-49.08 20.093-58.05 0-96.46-44.889-96.46-115.003 0-70.113 44.39-115.43 98.17-115.43 15.79 0 30.3 4.275 44.38 14.963 5.98 4.275 12.38.855 12.38-5.986v-3.847h42.26V363.54c0 72.678-43.97 103.46-93.9 103.46m-2.56-132.959c19.2 0 33.29-8.55 44.81-20.949 7.26-8.122 9.39-13.68 9.39-26.506v-61.563c0-12.826-2.13-20.948-10.67-29.071-9.39-8.978-22.62-14.964-39.69-14.964-35 0-61.89 29.072-61.89 76.954 0 47.883 24.76 76.099 58.05 76.099M1455.92 199.372c0-7.268-5.97-13.253-13.23-13.253h-32.44v-38.477h32.44c7.26 0 13.23-5.985 13.23-13.253v-5.986c0-45.744 23.48-68.403 69.15-68.403h29.02v38.477h-29.45c-17.5 0-26.46 9.833-26.46 29.926v5.986c0 7.268 5.97 13.253 13.23 13.253h42.68v38.477h-42.68c-7.26 0-13.23 5.985-13.23 13.253v176.994h-42.26zM1652.02 381.496c-35.85 0-69.14-23.086-69.14-75.671V147.642h42.25v150.06c0 29.499 17.07 44.889 37.13 44.889 21.77 0 35.85-11.97 50.79-29.499 7.26-8.977 9.82-19.238 9.82-30.354V147.642h42.25v228.724h-42.25V356.7c0-5.131-6.4-6.841-10.24-2.993-13.24 12.826-31.59 27.789-60.61 27.789M1893.57 381.496c-38.84 0-79.39-19.239-90.06-65.838h43.54c6.4 17.528 23.9 29.498 44.81 29.498 23.05 0 37.13-13.68 37.13-30.353 0-16.246-11.09-25.224-28.59-30.354l-36.28-10.261c-31.58-8.978-55.06-29.499-55.06-64.556 0-38.049 35.43-67.12 75.55-67.12 32.01 0 70.85 14.535 81.09 65.41h-40.55c-5.55-17.528-20.06-29.071-40.54-29.071-20.06 0-34.58 12.398-34.58 28.216 0 13.253 8.11 23.086 27.32 28.644l34.15 9.833c32.43 9.406 58.47 29.072 58.47 66.693 0 39.332-34.15 69.259-76.4 69.259M2098.54 381.496c-61.46 0-102.01-51.73-102.01-119.706s43.11-119.278 101.58-119.278c63.6 0 96.89 51.302 96.89 109.872v23.087h-144.26c-5.98 0-8.54 3.847-7.26 13.68 4.7 32.064 30.31 54.295 55.49 54.295 18.78 0 35-9.405 45.67-27.788h44.81c-16.22 40.187-49.94 65.838-90.91 65.838m43.11-141.51c6.83 0 9.39-3.42 7.68-14.108-4.69-26.506-24.33-45.317-51.22-45.317-25.6 0-47.37 18.811-54.2 45.745-2.56 9.833.85 13.68 6.83 13.68z\"></path><path fill=\"#FF5D5F\" d=\"m286.292 286.105 34.597 27.791s26.473-19.661 45.941-22.545c20.418-3.025 42.202 8.359 62.388 21.93 30.489 20.498 56.149 46.508 56.149 46.508l30.06-29.493s-82.879-89.795-148.597-81.672c-43.105 5.328-80.538 37.481-80.538 37.481\"></path><path fill=\"#4E9CFF\" d=\"M88.358 114.862 60 146.056s79.009 73.732 141.224 73.732c28.358 0 67.684-22.216 101.523-51.079 19.283-16.448 40.835-35.13 62.388-35.13 14.487 0 33.594 7.673 51.612 27.824 0 0 11.63-6.974 18.716-11.985 6.228-4.404 15.479-11.91 15.479-11.91-25.918-27.663-63.407-47.883-85.807-45.9-36.299.005-62.388 22.601-94.717 48.735s-45.94 36.907-69.194 36.907c-39.134 0-112.866-62.388-112.866-62.388M88.358 352.463 60 321.269s79.009-73.732 141.224-73.732c28.358 0 67.684 22.216 101.523 51.079 19.283 16.448 40.835 35.13 62.388 35.13 14.556 0 33.518-7.989 51.612-28.358 0 0 10.877 6.705 17.582 11.344 6.894 4.769 17.015 12.655 17.015 12.655-25.931 27.883-63.693 48.323-86.209 46.33-36.299-.005-57.851-19.24-90.179-45.374-32.329-26.133-50.478-40.268-73.732-40.268-39.134 0-112.866 62.388-112.866 62.388M458.142 185.149c-7.378 5.1-19.283 12.478-19.283 12.478s6.806 14.746 6.806 34.597-6.239 36.866-6.239 36.866 10.688 6.675 17.582 11.343c7.162 4.849 18.149 13.045 18.149 13.045s13.045-27.224 13.045-61.254-13.045-59.552-13.045-59.552-10.236 7.792-17.015 12.477\"></path><path fill=\"#FF5D5F\" d=\"m287.995 180.612 32.895-27.224s26.473 19.046 45.941 21.93c20.417 3.026 42.202-8.359 62.388-21.93 30.489-20.498 56.149-46.507 56.149-46.507l30.06 29.492s-82.879 89.795-148.597 81.672c-43.105-5.328-78.836-37.433-78.836-37.433M208.601 91c42.538 0 78.264 36.299 78.264 36.299s-9.941 7.832-16.448 13.045c-6.777 5.429-17.582 14.179-17.582 14.179s-18.711-19.851-44.234-19.851c-10.465 0-24.066 6.286-38.567 18.716-11.188 9.591-22.829 21.514-30.627 36.299-6.743 12.784-10.42 27.85-10.776 43.672-.446 19.873 6.597 40.704 18.149 57.283 7.743 11.112 16.983 19.474 26.657 26.657 12.555 9.322 25.648 15.881 35.164 15.881 10.166 0 19.306-3.533 26.09-6.806 10.776-6.239 19.278-13.612 19.278-13.612l33.463 27.791s-13.612 13.612-32.323 23.821c-12.091 5.963-27.632 11.91-46.508 11.91-18.862 0-40.767-10.022-61.254-25.522-13.244-10.021-26.225-21.895-36.298-36.299-16.51-23.607-25.017-52.328-24.96-81.104.057-29.136 9.451-57.993 26.094-81.672C138.273 117.657 176.86 91 208.601 91\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 4 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->4</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->4<!-- --> earned</span></div><div class=\"skillTracker__series\">Agent orchestration with LangGraph</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">State that survives a restart</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">2</span><span class=\"skillTracker__skill\" data-state=\"current\">Hand work between agents</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Governed tools an agent can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Engineer the context, not the prompt</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>You have two agents. One books appointments. One handles billing.</p>\n<p>A ticket arrives that needs both: <em>reschedule my install, and my bill looks wrong.</em>\nSo you send it to both.</p>\n<p>The first one books Tuesday. The second one sees the account is past due and\nfreezes it. Both finish at almost the same moment, and both save what they decided.</p>\n<p>Only one of them is saved. Which one? Whichever finished first — which comes down to\nhow slow an API call was that day. So you book an appointment on a frozen account,\nor freeze an account you just promised an engineer to. Afterwards it looks like one\nclean decision was made.</p>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/\">Part 1</a> built one agent that runs\nits steps in a fixed order. This post has several: a supervisor that picks who works\non what, an agent that passes the job to another one halfway through, and two agents\nput in each other's way on purpose — to find out what LangGraph does when they\ndisagree.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>Verified versions: Python 3.10.12 · <code>langgraph</code> 1.2.11 ·\n<code>langgraph-checkpoint</code> 4.2.0 · <code>langgraph-checkpoint-postgres</code> 3.1.2 ·\n<code>psycopg</code> 3.3.4 · <code>langchain-openai</code> 1.6.0 · <code>langfuse</code> 4.14.5 against Langfuse\nserver v3.205.1 OSS · image <code>postgres:17</code>. The <code>lg-checkpoints</code> container and\nvirtualenv from Part 1 are reused unchanged.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">A WEC Instance — <a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/compute/instances/compute_instance/\">deploy one</a> if you have not already</li>\n<li class=\"\">A WEC Inference API key — <a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/inference/api_keys/\">create one</a> in the portal</li>\n<li class=\"\">The Postgres container and virtualenv from <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/\">Part 1</a></li>\n<li class=\"\">A Langfuse instance for the last section — see <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/\">Observe production with Langfuse</a></li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"someone-has-to-decide-who-works-on-it\">Someone has to decide who works on it<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#someone-has-to-decide-who-works-on-it\" class=\"hash-link\" aria-label=\"Direct link to Someone has to decide who works on it\" title=\"Direct link to Someone has to decide who works on it\" translate=\"no\">​</a></h2>\n<p>Two agents means something has to choose between them. That thing is called a\nsupervisor, and it is just another agent whose only job is picking the next one.</p>\n<p>Part 1 wired its steps together in advance with <code>add_edge</code> — step one, then two,\nthen three. A supervisor cannot do that, because it does not know where the work\nis going until it reads it.</p>\n<p>Instead the node returns a <code>Command</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">types </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Command</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">supervisor</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">Literal</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    nxt </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"invoice\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"ticket\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">lower</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"supervisor routing to </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nxt</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">goto</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">nxt</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> update</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"supervisor-&gt;</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nxt</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p><code>Command</code> does two jobs at once: <code>goto</code> names the next node, and <code>update</code> carries\na state change with it. There is <strong>no <code>add_edge</code> from <code>supervisor</code> to anything</strong> —\nrouting happens at runtime, from inside the function.</p>\n<p>Save the whole thing as <code>lg_super.py</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">lg_super.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> sys</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> operator </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> add</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Annotated</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> Literal</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing_extensions </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> TypedDict</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">graph </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> StateGraph</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> START</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> END</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">checkpoint</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">postgres </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> PostgresSaver</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">types </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Command</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">DB_URI </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"postgresql://langgraph:changeme@127.0.0.1:5434/checkpoints\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">THREAD </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">len</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"sup-1\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">TICKET </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">len</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"reschedule my install\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">TypedDict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    ticket</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    completed</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Annotated</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> add</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">supervisor</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">Literal</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    nxt </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"invoice\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"ticket\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">lower</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"supervisor routing to </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nxt</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">goto</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">nxt</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> update</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"supervisor-&gt;</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nxt</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">scheduler</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler running\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">billing</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"billing running\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> StateGraph</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"supervisor\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> supervisor</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> scheduler</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> billing</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">START</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"supervisor\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> END</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> END</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"--draw\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token builtin\">compile</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get_graph</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">draw_mermaid</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">raise</span><span class=\"token plain\"> SystemExit</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> PostgresSaver</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">from_conn_string</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">DB_URI</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> cp</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    cp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">setup</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    graph </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token builtin\">compile</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">checkpointer</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">cp</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    config </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"configurable\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"thread_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> THREAD</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"FINAL:\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"ticket\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> TICKET</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>The <code>--draw</code> branch prints the graph and exits before touching Postgres, so you can\ninspect the topology without a database. It earns its place in the next section.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_super.py sup-1 </span><span class=\"token string\" style=\"color:#e3116c\">\"reschedule my install\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor routing to scheduler</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">scheduler running</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FINAL: {'ticket': 'reschedule my install', 'completed': ['supervisor-&gt;scheduler', 'scheduler']}</span><br></div></code></pre></div></div>\n<p>And the other branch, to show the routing is data-dependent rather than hardcoded:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_super.py sup-2 </span><span class=\"token string\" style=\"color:#e3116c\">\"please fix the invoice on my account\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor routing to billing</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">billing running</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FINAL: {'ticket': 'please fix the invoice on my account', 'completed': ['supervisor-&gt;billing', 'billing']}</span><br></div></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-annotation-that-is-not-type-hinting\">The annotation that is not type hinting<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#the-annotation-that-is-not-type-hinting\" class=\"hash-link\" aria-label=\"Direct link to The annotation that is not type hinting\" title=\"Direct link to The annotation that is not type hinting\" translate=\"no\">​</a></h3>\n<p>That return annotation looks like ordinary typing you could drop:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">supervisor</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">Literal</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><br></div></code></pre></div></div>\n<p>Drop it and the code runs identically — same routing, same output. So it is\noptional. Except it is not.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/ -&gt; Command\\[Literal\\[\"scheduler\", \"billing\"\\]\\]//'</span><span class=\"token plain\"> ~/lg_super.py </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> ~/lg_super_noann.py</span><br></div></code></pre></div></div>\n<p>Ask both versions to draw themselves:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_super.py </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--draw</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_super_noann.py </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--draw</span><br></div></code></pre></div></div>\n<p>With the annotation, the two possible routes are there as dotted conditional\nedges:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (with annotation)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">__start__ --&gt; supervisor;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor -.-&gt; billing;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor -.-&gt; scheduler;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">billing --&gt; __end__;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">scheduler --&gt; __end__;</span><br></div></code></pre></div></div>\n<p>Without it, they vanish, and the graph states something false:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (without annotation)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">__start__ --&gt; supervisor;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor --&gt; __end__;</span><br></div></code></pre></div></div>\n<p><code>scheduler</code> and <code>billing</code> are still declared as nodes, and now nothing reaches\nthem. The diagram says the supervisor goes straight to the end.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Two mermaid graphs side by side, one with dotted conditional edges to both agents and one where the supervisor connects straight to end\" src=\"https://development-wec.wiline.com/docs/assets/images/lg2-graphs-522fc91da9a7554bb7444d68736b3ee7.png\" width=\"585\" height=\"681\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> Same code, one annotation apart. The lower graph is wrong.</p>\n<p>The reason is mechanical: <code>goto</code> is decided inside the function body, where\nnothing can inspect it. The annotation is the <strong>only</strong> declaration of where a\nsupervisor is allowed to send work. Omit it and your code still works while every\ndiagram, and anything else reading the topology, quietly lies.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span><code>draw_ascii()</code> needs a package LangGraph does not ship</div><div class=\"admonitionContent_BuS1\"><p><code>get_graph().draw_ascii()</code> raises\n<code>ImportError: Install grandalf to draw graphs: 'pip install grandalf'</code>.\n<code>draw_mermaid()</code> has no extra dependency and is what the examples above use.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"handing-off-without-asking-the-supervisor\">Handing off without asking the supervisor<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#handing-off-without-asking-the-supervisor\" class=\"hash-link\" aria-label=\"Direct link to Handing off without asking the supervisor\" title=\"Direct link to Handing off without asking the supervisor\" translate=\"no\">​</a></h2>\n<p>A supervisor decides once, at the start. That is a problem when the discovery\nhappens later.</p>\n<p>Take a ticket asking to reschedule an install, on an account that turns out to be\npast due. The supervisor sees a scheduling request and routes accordingly. The\nscheduler starts work, reads the account, and finds a problem billing owns.</p>\n<p>The scheduler can route too — it is a node, and nodes return <code>Command</code>.</p>\n<p>Copy <code>lg_super.py</code> to <code>lg_handoff.py</code>, replace <code>scheduler</code> with the version below,\nand delete the <code>builder.add_edge(\"scheduler\", END)</code> line — the node decides its own\nexit now:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">lg_handoff.py (changed parts)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">scheduler</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">Literal</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"__end__\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"past due\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"ticket\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">lower</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler: account is past due, handing off to billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">goto</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> update</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler(handoff)\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler: booking it\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">goto</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">END</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> update</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler(booked)\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_handoff.py handoff-3 </span><span class=\"token string\" style=\"color:#e3116c\">\"reschedule my install, account is past due\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_handoff.py handoff-4 </span><span class=\"token string\" style=\"color:#e3116c\">\"reschedule my install\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor -&gt; scheduler</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">scheduler: account is past due, handing off to billing</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">billing running</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FINAL: {'ticket': '...past due', 'completed': ['supervisor', 'scheduler(handoff)', 'billing']}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor -&gt; scheduler</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">scheduler: booking it</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FINAL: {'ticket': 'reschedule my install', 'completed': ['supervisor', 'scheduler(booked)']}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Both handoff branches — one routing on to billing, one ending at the scheduler\" src=\"https://development-wec.wiline.com/docs/assets/images/lg2-handoff-4bb2edcb710dc048a8d846ee980848c2.png\" width=\"912\" height=\"193\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> The supervisor never knew billing would be involved.</p>\n<p>Two details worth keeping. <code>goto=END</code> works from inside a <code>Command</code>, so a node can\nfinish the run itself. And <code>\"__end__\"</code> is the string form of <code>END</code> in that\n<code>Literal</code> — the annotation needs the literal value, not the constant.</p>\n<p>This is the difference between the two published patterns. A <strong>supervisor</strong> decides\nfrom the outside and needs to know every precondition up front. A <strong>handoff</strong> lets\nthe agent doing the work redirect once it learns something. Most real systems want\nboth, which is what this graph has.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"sending-work-to-two-agents-at-once\">Sending work to two agents at once<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#sending-work-to-two-agents-at-once\" class=\"hash-link\" aria-label=\"Direct link to Sending work to two agents at once\" title=\"Direct link to Sending work to two agents at once\" translate=\"no\">​</a></h2>\n<p>So far the supervisor picks one agent. Sometimes you want both — check the calendar\nand the account at the same time, rather than waiting for one before starting the\nother. <code>goto</code> takes a list for exactly that:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">goto</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> update</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"supervisor\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Both run in the same super-step. Which is where it gets interesting, because now\nthey can disagree.</p>\n<p>Copy <code>lg_super.py</code> to <code>lg_conflict.py</code>. Give the state two kinds of field:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">lg_conflict.py (changed parts)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">TypedDict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    completed</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Annotated</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> add</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># has a reducer</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    decision</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token plain\">                          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># no reducer</span><br></div></code></pre></div></div>\n<p>And have each agent write a different value to the un-reduced one:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">scheduler</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"decision\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"book tuesday\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">billing</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"decision\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"refund first\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_conflict.py conflict-2</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor sending work to BOTH</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">billing deciding</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">scheduler deciding</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Traceback (most recent call last):</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  File \".../langgraph/pregel/_loop.py\", line 692, in after_tick</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    self.updated_channels = apply_writes(</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  File \".../langgraph/channels/last_value.py\", line 64, in update</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    raise InvalidUpdateError(msg)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langgraph.errors.InvalidUpdateError: At key 'decision': Can receive only one value</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">per step. Use an Annotated key to handle multiple values.</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The InvalidUpdateError traceback ending at last_value.py with the &amp;#39;At key decision&amp;#39; message\" src=\"https://development-wec.wiline.com/docs/assets/images/lg2-conflict-906aa9ed90d5c25e9021f93fef593bcf.png\" width=\"993\" height=\"328\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> Both agents finished. The graph refused to merge their answers.</p>\n<p>Read the trace carefully, because three things in it matter.</p>\n<p><strong>Both agents ran.</strong> <code>billing deciding</code> and <code>scheduler deciding</code> both printed. The\nwork completed; the failure came afterwards, in <code>after_tick</code> → <code>apply_writes</code>. This\nis not two agents colliding mid-flight. It is the graph refusing to reconcile them\nat the step boundary.</p>\n<p><strong><code>completed</code> was fine.</strong> Both agents appended to it in the same step and nothing\ncomplained, because <code>add</code> says what two values mean. Only <code>decision</code> failed.</p>\n<p><strong>LangGraph does not pick a winner.</strong> No last-write-wins, no silent choice. Which\nis the right behaviour — the alternative is a booking agent that sometimes books\nTuesday and sometimes issues a refund depending on which task happened to finish\nfirst.</p>\n<p>Note the order too: <code>billing</code> printed before <code>scheduler</code>, the reverse of the <code>goto</code>\nlist. Parallel branches finish in whatever order they finish.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"stating-the-policy\">Stating the policy<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#stating-the-policy\" class=\"hash-link\" aria-label=\"Direct link to Stating the policy\" title=\"Direct link to Stating the policy\" translate=\"no\">​</a></h3>\n<p>The fix is not to prevent the disagreement. It is to say what a disagreement\nmeans. Copy <code>lg_conflict.py</code> to <code>lg_resolve.py</code> and add the reducer:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">lg_resolve.py (changed parts)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">PRIORITY </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"refund first\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"book tuesday\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">prefer_higher_priority</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">old</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> new</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"The policy: a billing hold outranks a scheduling decision.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    winner </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">max</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">old</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> new</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token keyword\" style=\"color:#00009f\">lambda</span><span class=\"token plain\"> v</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> PRIORITY</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">v</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"  reducer: '</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">old</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">' vs '</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">new</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">' -&gt; '</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">winner</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">'\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> winner</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">TypedDict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    completed</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Annotated</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> add</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    decision</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Annotated</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> prefer_higher_priority</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_resolve.py resolve-2</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  reducer: '' vs '' -&gt; ''</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor sending work to BOTH</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">billing deciding</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">scheduler deciding</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  reducer: '' vs 'refund first' -&gt; 'refund first'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  reducer: 'refund first' vs 'book tuesday' -&gt; 'refund first'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FINAL: {'completed': ['supervisor', 'billing', 'scheduler'], 'decision': 'refund first'}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The reducer firing three times, folding two agent decisions into one winner\" src=\"https://development-wec.wiline.com/docs/assets/images/lg2-resolve-e9d09a07776b7e33a95a0f951453e628.png\" width=\"624\" height=\"158\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 4.</strong> The print inside the reducer makes the merge visible.</p>\n<p>Same graph, same disagreement, no error. And the trace shows three things the\ndocumentation does not spell out:</p>\n<p><strong>The reducer runs pairwise, folded left.</strong> Not once with both values — twice,\naccumulating. First it merges the existing state with billing's answer, then merges\nthat result with scheduler's.</p>\n<p><strong>It must therefore be order-independent.</strong> Since branches finish in arbitrary\norder, a reducer like \"take the newest\" produces different answers on different\nruns. <code>max</code> by priority does not.</p>\n<p><strong>It fires before anything runs.</strong> That first <code>reducer: '' vs '' -&gt; ''</code> is the\ninitial state being written, merging the default with the <code>decision: \"\"</code> passed to\n<code>invoke</code>. Your reducer sees empty input and must tolerate it.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"crashing-halfway-through-a-parallel-step\">Crashing halfway through a parallel step<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#crashing-halfway-through-a-parallel-step\" class=\"hash-link\" aria-label=\"Direct link to Crashing halfway through a parallel step\" title=\"Direct link to Crashing halfway through a parallel step\" translate=\"no\">​</a></h2>\n<p>Part 1 showed a killed run resuming from its last completed node. Parallel work\nraises a sharper question: if one branch finishes and its sibling dies, is the\nfinished work lost?</p>\n<p>Copy <code>lg_conflict.py</code> to <code>lg_partial.py</code>. Drop the <code>decision</code> field, add\n<code>RESUME = \"--resume\" in sys.argv</code>, pass <code>None if RESUME else {...}</code> to <code>invoke</code> as\nin Part 1, and replace the two specialists with one instant node and one slow\nenough to kill:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">lg_partial.py (changed parts)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">fast</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"fast running — finishes immediately\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"fast\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">slow</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"slow running — 30s window, KILL ME HERE\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">sleep</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token number\" style=\"color:#36acaa\">30</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"slow\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_partial.py partial-2</span><br></div></code></pre></div></div>\n<p>Press Ctrl+C during the window. It takes two presses, and the run ends on\nsomething that looks much worse than a <code>KeyboardInterrupt</code>:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (tail)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  File \".../langgraph/pregel/_runner.py\", line 613, in commit</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    self.put_writes()(task.id, task.writes)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">RuntimeError: cannot schedule new futures after shutdown</span><br></div></code></pre></div></div>\n<p>That is <code>fast</code>'s checkpoint write being committed to an executor that has already\nshut down. It reads like data loss.</p>\n<p>It is not:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">~/.venv/bin/python ~/lg_partial.py partial-2 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--resume</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">BEFORE  values={'completed': ['supervisor', 'fast']}  next=('slow',)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">slow running — 30s window, KILL ME HERE</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FINAL: {'completed': ['supervisor', 'fast', 'slow']}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The resumed run showing fast recovered from Postgres and only slow pending\" src=\"https://development-wec.wiline.com/docs/assets/images/lg2-partial-13329c0354dca69472571333fd73fb02.png\" width=\"657\" height=\"100\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 5.</strong> <code>fast</code> recovered, did not re-run, and <code>next</code> names only the branch that died.</p>\n<p><code>fast</code> is in the recovered state, appears exactly once in the final result, and\n<code>next=('slow',)</code> names only the branch that never finished. The write had already\nlanded in <code>checkpoint_writes</code> — the same table that stopped <code>step_one</code> re-running\nin Part 1 now protects a completed sibling.</p>\n<p>Worth documenting precisely because the error suggests the opposite. A\n<code>RuntimeError</code> in a shutdown path is noise; the state is the thing to check.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"a-model-as-the-router\">A model as the router<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#a-model-as-the-router\" class=\"hash-link\" aria-label=\"Direct link to A model as the router\" title=\"Direct link to A model as the router\" translate=\"no\">​</a></h2>\n<p>Every routing decision so far has been <code>\"invoice\" in ticket</code> — keyword matching.\nThat is the approach we measured coming apart on\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/\">the gateway's complexity router</a>, where a\nplural is enough to miss a keyword entirely.</p>\n<p>So let the model decide. Copy <code>lg_super.py</code> to <code>lg_traced.py</code> — same graph, one\nnode changed:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">lg_traced.py (changed parts)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">llm </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ChatOpenAI</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"gemma4\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    base_url</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"https://inference.wiline.com/v1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"WEC_API_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    temperature</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">supervisor</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">Literal</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> llm</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"Route this support ticket to exactly one team. \"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"Answer with one word, either scheduler or billing, nothing else.\\n\\n\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Ticket: </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">state</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'ticket'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    raw </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">strip</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">lower</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    nxt </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> raw </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"scheduler\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"supervisor: model said </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">raw</span><span class=\"token string-interpolation interpolation conversion-option punctuation\" style=\"color:#393A34\">!r</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> -&gt; </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nxt</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">goto</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">nxt</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> update</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"supervisor-&gt;</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">nxt</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Credentials come from the environment, which the gateway's <code>.env</code> already holds:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> -a</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">.</span><span class=\"token plain\"> ~/llm-gateway/.env</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> ~/.venv/bin/python ~/lg_traced.py traced-1</span><br></div></code></pre></div></div>\n<p>The ticket is deliberately ambiguous — <em>\"my bill looks wrong and I need a new\ninstall date\"</em> mentions both:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">supervisor: model said 'billing' -&gt; billing</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">billing running</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FINAL: {'ticket': 'my bill looks wrong and I need a new install date', 'completed': ['supervisor-&gt;billing', 'billing']}</span><br></div></code></pre></div></div>\n<p>One word, no wrapper sentence, and the parse held. It picked billing — defensible,\nsince an unpaid account gates the install, which is the same conclusion the handoff\nreached earlier by reading the account.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-that-one-word-cost\">What that one word cost<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#what-that-one-word-cost\" class=\"hash-link\" aria-label=\"Direct link to What that one word cost\" title=\"Direct link to What that one word cost\" translate=\"no\">​</a></h3>\n<p>Asking a model to choose is not free, and the bill is not where you would look for\nit. Turning on tracing shows where the time and the tokens went. It is one line, as\nin Part 1 — <code>CallbackHandler()</code> reads its credentials from the environment and goes\nin the same config dict as the thread id:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langfuse</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">langchain </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> CallbackHandler</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">handler </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> CallbackHandler</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">config </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"configurable\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"thread_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> THREAD</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"callbacks\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">handler</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Langfuse trace showing supervisor and billing spans, the token badge, the serialised Command with goto, and billing receiving the supervisor&amp;#39;s state update\" src=\"https://development-wec.wiline.com/docs/assets/images/lg2-trace-771785326bcdaf3d0132233cc7bdf9fc.png\" width=\"1903\" height=\"998\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 6.</strong> Three things boxed: what routing cost, the decision as data, and the payload arriving.</p>\n<p>The span tree is the graph:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">LangGraph        8.66s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  supervisor     8.53s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    ChatOpenAI   8.49s   52 → 412 (Σ 464)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  billing        0.01s</span><br></div></code></pre></div></div>\n<p>Three findings sit in that frame.</p>\n<p><strong>A one-word answer cost 412 output tokens.</strong> Confirmed against the API rather\nthan read off the screen:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> -a</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">.</span><span class=\"token plain\"> ~/llm-gateway/.env</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-u</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$LANGFUSE_PUBLIC_KEY</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string variable\" style=\"color:#36acaa\">$LANGFUSE_SECRET_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$LANGFUSE_HOST</span><span class=\"token string\" style=\"color:#e3116c\">/api/public/observations?limit=8\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'.data[] | select(.name==\"ChatOpenAI\") | {model, usage}'</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"model\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"gemma4\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"usage\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"unit\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"TOKENS\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"input\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">52</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"output\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">412</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"total\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">464</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>The visible answer is seven characters. The bill is 412 output tokens. Whatever the\nmodel generated before settling, you paid for — and a routing call looks like the\ncheapest thing in the graph.</p>\n<p><strong>The supervisor is the expensive node, not the specialists.</strong> <code>billing</code> took 6ms\nand no tokens. The router took 8.49s and 464. In a multi-agent graph, the\norchestration decisions are the cost centre.</p>\n<p><strong>The routing decision is recorded as data.</strong> The supervisor's output in the trace\nis the serialised <code>Command</code>:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"graph\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token null keyword\" style=\"color:#00009f\">null</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"update\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"completed\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"supervisor-&gt;billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"resume\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token null keyword\" style=\"color:#00009f\">null</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"goto\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"billing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>And <code>billing</code>'s input contains <code>[\"supervisor-&gt;billing\"]</code> — the <code>update</code> from that\n<code>Command</code> arriving as the receiving agent's state. Handoff, cost and payload in one\nframe.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>A trace shows the path taken, not the graph</div><div class=\"admonitionContent_BuS1\"><p><code>scheduler</code> appears nowhere in this trace — not as a skipped span, not as a\ndisabled node. The Graph panel draws <code>__start__ → supervisor → billing → __end__</code>.\nYou cannot tell from a trace which alternatives existed, only which one ran, which\nis a second reason the missing <code>Literal</code> annotation matters: the drawn graph is the\nonly place your alternatives are written down.</p></div></div>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Hand work between agents</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>Part 3: the tools an agent is allowed to call. Everything above trusts each node\nto do only what it should — the next post puts the tools behind a gateway with\nper-tool permissions, so an agent's capabilities are granted rather than assumed.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting\">Troubleshooting<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#troubleshooting\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting\" title=\"Direct link to Troubleshooting\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"importerror-install-grandalf-to-draw-graphs\"><code>ImportError: Install grandalf to draw graphs</code><a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#importerror-install-grandalf-to-draw-graphs\" class=\"hash-link\" aria-label=\"Direct link to importerror-install-grandalf-to-draw-graphs\" title=\"Direct link to importerror-install-grandalf-to-draw-graphs\" translate=\"no\">​</a></h3>\n<p><code>draw_ascii()</code> needs a package LangGraph does not depend on. Use <code>draw_mermaid()</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-graph-diagram-shows-the-supervisor-going-straight-to-__end__\">The graph diagram shows the supervisor going straight to <code>__end__</code><a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#the-graph-diagram-shows-the-supervisor-going-straight-to-__end__\" class=\"hash-link\" aria-label=\"Direct link to the-graph-diagram-shows-the-supervisor-going-straight-to-__end__\" title=\"Direct link to the-graph-diagram-shows-the-supervisor-going-straight-to-__end__\" translate=\"no\">​</a></h3>\n<p>The routing node is missing its <code>-&gt; Command[Literal[...]]</code> return annotation. The\ncode still runs; only the topology is wrong.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"invalidupdateerror-at-key-x-can-receive-only-one-value-per-step\"><code>InvalidUpdateError: At key 'x': Can receive only one value per step</code><a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#invalidupdateerror-at-key-x-can-receive-only-one-value-per-step\" class=\"hash-link\" aria-label=\"Direct link to invalidupdateerror-at-key-x-can-receive-only-one-value-per-step\" title=\"Direct link to invalidupdateerror-at-key-x-can-receive-only-one-value-per-step\" translate=\"no\">​</a></h3>\n<p>Two nodes wrote the same key in one super-step and that key has no reducer. Either\ngive it one with <code>Annotated[T, fn]</code>, or stop dispatching both nodes in parallel.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"runtimeerror-cannot-schedule-new-futures-after-shutdown-after-ctrlc\"><code>RuntimeError: cannot schedule new futures after shutdown</code> after Ctrl+C<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#runtimeerror-cannot-schedule-new-futures-after-shutdown-after-ctrlc\" class=\"hash-link\" aria-label=\"Direct link to runtimeerror-cannot-schedule-new-futures-after-shutdown-after-ctrlc\" title=\"Direct link to runtimeerror-cannot-schedule-new-futures-after-shutdown-after-ctrlc\" translate=\"no\">​</a></h3>\n<p>A completed branch's checkpoint write raced the executor shutting down. Check\n<code>graph.get_state(config).values</code> before assuming anything was lost — in testing the\nwrite had landed every time.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-reducer-produces-different-results-on-different-runs\">The reducer produces different results on different runs<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#the-reducer-produces-different-results-on-different-runs\" class=\"hash-link\" aria-label=\"Direct link to The reducer produces different results on different runs\" title=\"Direct link to The reducer produces different results on different runs\" translate=\"no\">​</a></h3>\n<p>It is not order-independent. Parallel branches finish in arbitrary order, so a\nreducer that depends on which value arrives second is non-deterministic by\nconstruction.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/\">Part 1 — checkpoint a LangGraph agent</a> — state that survives a crash</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/\">Route by complexity</a> — what keyword routing misses, measured</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/\">Component-level tracing for agent tool calls</a> — finding which step failed</li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/",
            "title": "Hand work between LangGraph agents without corrupting shared state",
            "summary": "A supervisor routes work to specialists, an agent hands off mid-task, and two agents write the same state key in the same step. One of those raises an error rather than picking a winner — and the routing decision costs 412 output tokens to say one word.",
            "date_modified": "2026-08-26T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "agents",
                "langgraph",
                "multi-agent",
                "state",
                "postgres",
                "langfuse",
                "self-hosting",
                "wec",
                "wec-inference"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/",
            "content_html": "<div class=\"tutorialHero\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"487\" height=\"249\" fill=\"none\" viewBox=\"0 0 487 249\" class=\"tutorialHero__brand\"><path fill=\"#1C3C3C\" fill-rule=\"evenodd\" d=\"M124.273.412h239.031c68.111 0 123.52 55.603 123.52 123.948s-55.409 123.948-123.52 123.948H124.273c-68.11 0-123.52-55.603-123.52-123.948S56.164.412 124.274.412M234.018 192.55c3.001 3.156 7.443 3 11.378 2.182l.039.02c1.827-1.486-.77-3.368-3.249-5.165-1.487-1.077-2.932-2.125-3.356-3.038 1.373-1.674-2.687-5.474-5.847-8.433-1.327-1.242-2.495-2.335-3.037-3.061-2.248-2.448-3.152-5.537-4.062-8.644-.603-2.061-1.208-4.131-2.211-6.027-6.176-14.339-13.248-28.561-23.165-40.699-6.373-8.067-13.646-15.291-20.922-22.518-4.691-4.66-9.382-9.319-13.835-14.206-4.581-4.73-7.338-10.556-10.1-16.392-2.312-4.886-4.627-9.779-8.018-14.04-10.268-15.196-42.686-19.346-47.44 2.124.019.662-.195 1.09-.78 1.52-2.63 1.928-4.968 4.11-6.935 6.76-4.812 6.721-5.553 18.118.448 24.158l.025-.388c.2-3.05.388-5.9 2.8-8.087 4.637 3.994 11.67 5.416 17.047 2.435 6.483 9.3 8.548 20.543 10.621 31.823 1.726 9.398 3.457 18.821 7.751 27.171l.266.443c2.524 4.202 5.089 8.472 8.326 12.142 1.176 1.823 3.59 3.791 6 5.755 3.18 2.591 6.353 5.177 6.663 7.416.015.974.01 1.961.006 2.954-.025 5.879-.051 11.967 3.716 16.801 2.084 4.228-3.02 8.475-7.131 7.949-2.254.312-4.716-.282-7.161-.871-3.346-.807-6.659-1.607-9.36-.064-.758.82-1.846.849-2.94.877-1.297.035-2.601.069-3.373 1.422-.158.402-.528.856-.913 1.328-.846 1.036-1.762 2.159-.665 3.016q.149-.111.294-.224c1.663-1.269 3.248-2.479 5.493-1.724-.299 1.658.772 2.103 1.843 2.547q.281.114.553.239c-.011.385-.087.774-.163 1.159-.18.921-.356 1.824.358 2.62.339-.344.639-.732.939-1.12.735-.95 1.474-1.904 2.802-2.25 2.92 3.9 5.862 2.28 9.554.248 4.164-2.293 9.281-5.111 16.396-1.125-2.727-.136-5.163.195-6.994 2.455-.448.507-.838 1.091-.039 1.754 4.21-2.728 5.961-1.748 7.61-.825 1.19.666 2.326 1.302 4.294.493.465-.242.93-.493 1.396-.745 3.161-1.705 6.366-3.434 10.118-2.839-2.803.808-3.8 2.584-4.888 4.524-.539.959-1.099 1.957-1.911 2.898-.429.429-.624.936-.137 1.656 5.869-.488 8.087-1.976 11.083-3.986 1.429-.959 3.036-2.038 5.302-3.183 2.505-1.542 5.009-.556 7.436.4 2.633 1.037 5.175 2.037 7.527-.264.743-.7 1.674-.708 2.602-.717a10 10 0 0 0 1.002-.043c-.732-3.92-4.861-3.874-9.052-3.827-4.847.054-9.778.109-9.632-5.972 4.504-3.076 4.546-8.415 4.585-13.461.01-1.218.019-2.419.091-3.567 3.313 1.847 6.817 3.291 10.299 4.725 3.276 1.35 6.533 2.692 9.593 4.354 3.195 5.143 8.182 11.962 14.826 11.514.175-.526.331-.974.526-1.5.383.067.787.169 1.199.273 1.743.442 3.611.915 4.509-1.15m130.213-58.45a20.54 20.54 0 0 0 14.504 5.994c5.44 0 10.658-2.156 14.505-5.994a20.44 20.44 0 0 0 6.007-14.469 20.44 20.44 0 0 0-6.007-14.469 20.54 20.54 0 0 0-21.882-4.624l-11.757-17.162-8.194 5.614 11.818 17.25a20.43 20.43 0 0 0-5.002 13.391 20.43 20.43 0 0 0 6.008 14.469m-36.808-55.576a20.55 20.55 0 0 0 21.408-1.964 20.46 20.46 0 0 0 7.34-10.483 20.4 20.4 0 0 0-.331-12.784 20.47 20.47 0 0 0-7.873-10.09 20.557 20.557 0 0 0-18.35-2.282 20.5 20.5 0 0 0-7.964 5.175 20.44 20.44 0 0 0-4.77 8.201 20.429 20.429 0 0 0 3.232 18.164 20.5 20.5 0 0 0 7.308 6.063m0 118.824a20.55 20.55 0 0 0 21.408-1.964 20.46 20.46 0 0 0 7.34-10.483 20.4 20.4 0 0 0-.331-12.783 20.47 20.47 0 0 0-7.873-10.092 20.55 20.55 0 0 0-26.314 2.894 20.45 20.45 0 0 0-4.77 8.201 20.43 20.43 0 0 0 3.232 18.164 20.5 20.5 0 0 0 7.308 6.063m18.857-72.629v-10.174h-31.394a20.3 20.3 0 0 0-4.398-8.342L322.3 88.704l-8.59-5.698-11.812 17.499a20.5 20.5 0 0 0-6.749-1.221 20.5 20.5 0 0 0-14.462 5.959 20.3 20.3 0 0 0-5.99 14.388 20.3 20.3 0 0 0 5.99 14.387 20.5 20.5 0 0 0 14.462 5.96 20.5 20.5 0 0 0 6.749-1.221l11.812 17.498 8.487-5.697-11.709-17.498a20.3 20.3 0 0 0 4.398-8.342z\" clip-rule=\"evenodd\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/Postgresql_elephant.svg-62fedb3464ae364ae4e342c86b474ed6.png\" alt=\"PostgreSQL\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2255\" height=\"527\" fill=\"none\" viewBox=\"0 0 2255 527\" class=\"tutorialHero__langfuse\"><path fill=\"#1B1917\" d=\"M652.669 116.433c0-10.261-7.683-17.956-17.926-17.956H607V60h87.923v316.366h-42.254zM804.056 379.786c-39.693 0-72.131-27.362-72.131-68.831 0-41.042 28.596-69.686 84.935-69.686h39.693c7.256 0 12.805-5.558 12.805-12.826v-8.55c0-25.651-20.487-38.905-40.547-38.905-18.353 0-33.291 8.123-41.828 26.934h-45.668c11.95-44.462 46.095-65.41 88.349-65.41 40.547 0 82.374 22.231 82.374 77.808v156.046h-42.68v-14.109c0-5.13-5.549-7.695-9.817-4.702-16.646 11.97-32.438 22.231-55.485 22.231m5.975-38.477c18.78 0 34.572-9.406 52.498-27.362 4.695-4.702 6.829-10.688 6.829-17.1v-6.413c0-7.268-5.549-12.826-12.805-12.826H815.58c-27.743 0-40.974 12.398-40.974 31.637 0 17.956 12.377 32.064 35.425 32.064M961.855 376.366V147.642h42.255v19.666c0 5.13 6.4 6.84 10.24 2.992 13.23-12.825 31.59-27.788 60.61-27.788 36.71 0 70.85 23.086 70.85 75.671v158.183h-42.25V227.161c0-29.499-17.93-45.745-39.7-45.745-20.91 0-35 11.971-49.93 29.499-7.26 8.978-9.82 19.238-9.82 30.354v135.097zM1287.48 467c-52.5 0-85.79-24.796-95.61-63.273h46.1c7.25 15.818 20.06 25.651 45.67 25.651 34.14 0 55.48-20.093 55.48-65.838v-7.268c0-5.13-4.27-7.695-9.39-3.42-14.08 12.398-32.01 20.093-49.08 20.093-58.05 0-96.46-44.889-96.46-115.003 0-70.113 44.39-115.43 98.17-115.43 15.79 0 30.3 4.275 44.38 14.963 5.98 4.275 12.38.855 12.38-5.986v-3.847h42.26V363.54c0 72.678-43.97 103.46-93.9 103.46m-2.56-132.959c19.2 0 33.29-8.55 44.81-20.949 7.26-8.122 9.39-13.68 9.39-26.506v-61.563c0-12.826-2.13-20.948-10.67-29.071-9.39-8.978-22.62-14.964-39.69-14.964-35 0-61.89 29.072-61.89 76.954 0 47.883 24.76 76.099 58.05 76.099M1455.92 199.372c0-7.268-5.97-13.253-13.23-13.253h-32.44v-38.477h32.44c7.26 0 13.23-5.985 13.23-13.253v-5.986c0-45.744 23.48-68.403 69.15-68.403h29.02v38.477h-29.45c-17.5 0-26.46 9.833-26.46 29.926v5.986c0 7.268 5.97 13.253 13.23 13.253h42.68v38.477h-42.68c-7.26 0-13.23 5.985-13.23 13.253v176.994h-42.26zM1652.02 381.496c-35.85 0-69.14-23.086-69.14-75.671V147.642h42.25v150.06c0 29.499 17.07 44.889 37.13 44.889 21.77 0 35.85-11.97 50.79-29.499 7.26-8.977 9.82-19.238 9.82-30.354V147.642h42.25v228.724h-42.25V356.7c0-5.131-6.4-6.841-10.24-2.993-13.24 12.826-31.59 27.789-60.61 27.789M1893.57 381.496c-38.84 0-79.39-19.239-90.06-65.838h43.54c6.4 17.528 23.9 29.498 44.81 29.498 23.05 0 37.13-13.68 37.13-30.353 0-16.246-11.09-25.224-28.59-30.354l-36.28-10.261c-31.58-8.978-55.06-29.499-55.06-64.556 0-38.049 35.43-67.12 75.55-67.12 32.01 0 70.85 14.535 81.09 65.41h-40.55c-5.55-17.528-20.06-29.071-40.54-29.071-20.06 0-34.58 12.398-34.58 28.216 0 13.253 8.11 23.086 27.32 28.644l34.15 9.833c32.43 9.406 58.47 29.072 58.47 66.693 0 39.332-34.15 69.259-76.4 69.259M2098.54 381.496c-61.46 0-102.01-51.73-102.01-119.706s43.11-119.278 101.58-119.278c63.6 0 96.89 51.302 96.89 109.872v23.087h-144.26c-5.98 0-8.54 3.847-7.26 13.68 4.7 32.064 30.31 54.295 55.49 54.295 18.78 0 35-9.405 45.67-27.788h44.81c-16.22 40.187-49.94 65.838-90.91 65.838m43.11-141.51c6.83 0 9.39-3.42 7.68-14.108-4.69-26.506-24.33-45.317-51.22-45.317-25.6 0-47.37 18.811-54.2 45.745-2.56 9.833.85 13.68 6.83 13.68z\"></path><path fill=\"#FF5D5F\" d=\"m286.292 286.105 34.597 27.791s26.473-19.661 45.941-22.545c20.418-3.025 42.202 8.359 62.388 21.93 30.489 20.498 56.149 46.508 56.149 46.508l30.06-29.493s-82.879-89.795-148.597-81.672c-43.105 5.328-80.538 37.481-80.538 37.481\"></path><path fill=\"#4E9CFF\" d=\"M88.358 114.862 60 146.056s79.009 73.732 141.224 73.732c28.358 0 67.684-22.216 101.523-51.079 19.283-16.448 40.835-35.13 62.388-35.13 14.487 0 33.594 7.673 51.612 27.824 0 0 11.63-6.974 18.716-11.985 6.228-4.404 15.479-11.91 15.479-11.91-25.918-27.663-63.407-47.883-85.807-45.9-36.299.005-62.388 22.601-94.717 48.735s-45.94 36.907-69.194 36.907c-39.134 0-112.866-62.388-112.866-62.388M88.358 352.463 60 321.269s79.009-73.732 141.224-73.732c28.358 0 67.684 22.216 101.523 51.079 19.283 16.448 40.835 35.13 62.388 35.13 14.556 0 33.518-7.989 51.612-28.358 0 0 10.877 6.705 17.582 11.344 6.894 4.769 17.015 12.655 17.015 12.655-25.931 27.883-63.693 48.323-86.209 46.33-36.299-.005-57.851-19.24-90.179-45.374-32.329-26.133-50.478-40.268-73.732-40.268-39.134 0-112.866 62.388-112.866 62.388M458.142 185.149c-7.378 5.1-19.283 12.478-19.283 12.478s6.806 14.746 6.806 34.597-6.239 36.866-6.239 36.866 10.688 6.675 17.582 11.343c7.162 4.849 18.149 13.045 18.149 13.045s13.045-27.224 13.045-61.254-13.045-59.552-13.045-59.552-10.236 7.792-17.015 12.477\"></path><path fill=\"#FF5D5F\" d=\"m287.995 180.612 32.895-27.224s26.473 19.046 45.941 21.93c20.417 3.026 42.202-8.359 62.388-21.93 30.489-20.498 56.149-46.507 56.149-46.507l30.06 29.492s-82.879 89.795-148.597 81.672c-43.105-5.328-78.836-37.433-78.836-37.433M208.601 91c42.538 0 78.264 36.299 78.264 36.299s-9.941 7.832-16.448 13.045c-6.777 5.429-17.582 14.179-17.582 14.179s-18.711-19.851-44.234-19.851c-10.465 0-24.066 6.286-38.567 18.716-11.188 9.591-22.829 21.514-30.627 36.299-6.743 12.784-10.42 27.85-10.776 43.672-.446 19.873 6.597 40.704 18.149 57.283 7.743 11.112 16.983 19.474 26.657 26.657 12.555 9.322 25.648 15.881 35.164 15.881 10.166 0 19.306-3.533 26.09-6.806 10.776-6.239 19.278-13.612 19.278-13.612l33.463 27.791s-13.612 13.612-32.323 23.821c-12.091 5.963-27.632 11.91-46.508 11.91-18.862 0-40.767-10.022-61.254-25.522-13.244-10.021-26.225-21.895-36.298-36.299-16.51-23.607-25.017-52.328-24.96-81.104.057-29.136 9.451-57.993 26.094-81.672C138.273 117.657 176.86 91 208.601 91\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 4 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->4</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->4<!-- --> earned</span></div><div class=\"skillTracker__series\">Agent orchestration with LangGraph</div><ul class=\"skillTracker__steps\"><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">1</span><span class=\"skillTracker__skill\" data-state=\"current\">State that survives a restart</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-multi-agent-handoff-state/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Hand work between agents</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-governed-tools-agent-elicitation/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Governed tools an agent can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/scoped-mcp-tools-per-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Engineer the context, not the prompt</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Your agent has been working on a customer's request for forty seconds. It has read\nthe ticket, pulled the account, and checked three engineers' calendars. It is about\nto book the appointment.</p>\n<p>Then the process dies. A deploy going out, the host running out of memory, someone\nrestarting the container — it does not matter which.</p>\n<p>The agent does not pick up where it left off, because there is nothing to pick up\nfrom. Everything it learned lived in variables inside a process that no longer\nexists. The customer is still waiting. Run it again and you pay for all that work a\nsecond time. And the part that should worry you most: nobody can say whether the\nappointment was booked in the last second before it died.</p>\n<p>An agent is a model in a loop. As an ordinary Python script, that loop is exactly as\nfragile as the process holding it.</p>\n<p>Here we build one that saves its state to Postgres after every step, so a crash\ncosts nothing.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>Verified versions: Python 3.10.12 · <code>langgraph</code> 1.2.11 ·\n<code>langgraph-checkpoint</code> 4.2.0 · <code>langgraph-checkpoint-postgres</code> 3.1.2 ·\n<code>psycopg</code> 3.3.4 · <code>psycopg-pool</code> 3.3.1 · <code>langchain-openai</code> 1.6.0 ·\n<code>langfuse</code> 4.14.5 against Langfuse server v3.205.1 OSS · image <code>postgres:17</code>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-problem-before-the-solution\">The problem, before the solution<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#the-problem-before-the-solution\" class=\"hash-link\" aria-label=\"Direct link to The problem, before the solution\" title=\"Direct link to The problem, before the solution\" translate=\"no\">​</a></h2>\n<p>What you want is a <strong>resumable</strong> run: each step's result written somewhere durable\nthe moment it finishes, so a new process can carry on from there.</p>\n<p>That is what a checkpointer does.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"why-a-graph-and-not-a-loop\">Why a graph and not a loop<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#why-a-graph-and-not-a-loop\" class=\"hash-link\" aria-label=\"Direct link to Why a graph and not a loop\" title=\"Direct link to Why a graph and not a loop\" translate=\"no\">​</a></h3>\n<p>A loop with a <code>try/except</code> and a few database writes could do this for four fixed\nsteps. It stops working the moment the agent picks its own path: when the model\nchooses the next tool, \"where are we?\" has no answer you can write down.</p>\n<p>LangGraph makes you declare the work as a <strong>graph</strong> — named nodes, explicit edges.\nThat feels like ceremony until you want durability.</p>\n<table><thead><tr><th>A loop</th><th>A graph</th></tr></thead><tbody><tr><td>Steps are implicit in control flow</td><td>Steps are named nodes</td></tr><tr><td>\"Where am I?\" has no answer</td><td>Position is a value you can read</td></tr><tr><td>State lives in local variables</td><td>State is a declared object</td></tr><tr><td>Nothing to save</td><td>There is an obvious moment to save: node boundaries</td></tr></tbody></table>\n<p>The runtime knows when a node starts and ends, so it has an obvious moment to save.\nThe state is a declared object, so there is something definite to write.</p>\n<p>That is what \"a deterministic state machine around a non-deterministic model\" means:\nthe output is unpredictable, the next step is not. More on that debate in\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/news/loop-engineering-graph-engineering-what-survives/\">\"Loop Engineering Is Dead\"</a>.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-youll-build\">What you'll build<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#what-youll-build\" class=\"hash-link\" aria-label=\"Direct link to What you'll build\" title=\"Direct link to What you'll build\" translate=\"no\">​</a></h2>\n<!-- -->\n<p>Three properties the usual examples do not have:</p>\n<ul>\n<li class=\"\"><strong>state in Postgres</strong>, not in the process</li>\n<li class=\"\"><strong>a human approval gate</strong> the graph can sit at for days</li>\n<li class=\"\"><strong>every node traced</strong> as a span, with token counts and per-node latency</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">A WEC Instance — <a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/compute/instances/compute_instance/\">deploy one</a> if you have not already</li>\n<li class=\"\">A WEC Inference API key — <a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/inference/api_keys/\">create one</a> in the portal</li>\n<li class=\"\">A Langfuse instance — see <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/\">Observe production with Langfuse</a></li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--install-and-why-three-packages\">Step 1 — Install, and why three packages<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#step-1--install-and-why-three-packages\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Install, and why three packages\" title=\"Direct link to Step 1 — Install, and why three packages\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> venv .venv</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> .venv/bin/activate</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">pip </span><span class=\"token function\" style=\"color:#d73a49\">install</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"langgraph\"</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"langgraph-checkpoint-postgres\"</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"psycopg[binary,pool]\"</span><br></div></code></pre></div></div>\n<table><thead><tr><th>Package</th><th>What it does</th><th>Why you need it here</th></tr></thead><tbody><tr><td><code>langgraph</code></td><td>The graph runtime — nodes, edges, state</td><td>The orchestration itself</td></tr><tr><td><code>langgraph-checkpoint-postgres</code></td><td>Writes state to Postgres</td><td><strong>The point of this tutorial</strong></td></tr><tr><td><code>psycopg[binary,pool]</code></td><td>Postgres driver + connection pool</td><td><code>PostgresSaver</code> requires a pool</td></tr></tbody></table>\n<p>The middle row is worth pausing on. <strong>LangGraph's default persistence is\nin-memory.</strong> Durability is opt-in, in a separate package you have to know to\ninstall. That is precisely why so many published examples quietly lose state: they\nnever installed this, and nothing warned them.</p>\n<p>Three names pull in more than three packages:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">./.venv/bin/pip list </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-iE</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"langgraph|psycopg\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--version</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langgraph                     1.2.11</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langgraph-checkpoint          4.2.0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langgraph-checkpoint-postgres 3.1.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langgraph-prebuilt            1.1.0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langgraph-sdk                 0.4.3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">psycopg                       3.3.4</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">psycopg-binary                3.3.4</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">psycopg-pool                  3.3.1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Python 3.10.12</span><br></div></code></pre></div></div>\n<p><code>langgraph-checkpoint</code> is the one to notice: it is the abstract interface, and\n<code>-postgres</code> is one implementation of it. That split is why swapping Postgres for\nSQLite or Redis later changes one line.</p>\n<p>The <code>pool</code> extra is not decoration either. Install plain <code>psycopg</code> and the failure\ncomes at connect time, not install time — which is a much more confusing place to\nfind it.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"pip list showing the langgraph and psycopg packages with their versions and Python 3.10.12\" src=\"https://development-wec.wiline.com/docs/assets/images/lg1-versions-21d23b4acd78bb5e757cf1a9f3949f97.png\" width=\"1580\" height=\"380\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> Eight packages from three names — the checkpoint interface and its\nPostgres implementation arrive separately.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--a-database-for-checkpoints-not-for-the-app\">Step 2 — A database for checkpoints, not for the app<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#step-2--a-database-for-checkpoints-not-for-the-app\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — A database for checkpoints, not for the app\" title=\"Direct link to Step 2 — A database for checkpoints, not for the app\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--name</span><span class=\"token plain\"> lg-checkpoints </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">POSTGRES_USER</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">langgraph </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">POSTGRES_PASSWORD</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">changeme </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">POSTGRES_DB</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">checkpoints </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">127.0</span><span class=\"token plain\">.0.1:5434:5432 postgres:17</span><br></div></code></pre></div></div>\n<p>Three deliberate decisions:</p>\n<p><strong>A dedicated database.</strong> Checkpoints are write-heavy — a row per node transition,\nper thread, forever. Mixed into your application database they become a table you\nare afraid to truncate. Separate, they are disposable.</p>\n<p><strong><code>127.0.0.1:5434:5432</code>, not <code>-p 5434:5432</code>.</strong> The short form binds every\ninterface, and Docker writes its own iptables rules that bypass UFW — so a\nfirewall that looks correct is not protecting this port. Covered in\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\">Harden Docker networks</a>.</p>\n<p>The left number is the host port, the right the container's. On an empty instance\nuse <code>5432:5432</code>. If you already self-host anything with a database, that port is\nprobably taken — pick any free one.</p>\n<p><strong><code>postgres:17</code> pinned.</strong> <code>latest</code> means a reader six months from now runs\nsomething you never tested.</p>\n<p>Running is not the same as ready:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> lg-checkpoints pg_isready </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-U</span><span class=\"token plain\"> langgraph </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> checkpoints</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">/var/run/postgresql:5432 - accepting connections</span><br></div></code></pre></div></div>\n<p>A container reports <code>Up</code> the instant it starts, while Postgres inside is still\ninitialising. Connecting too early gives a connection error that looks like a\nconfiguration problem and is not.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--the-graph-deliberately-without-a-model\">Step 3 — The graph, deliberately without a model<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#step-3--the-graph-deliberately-without-a-model\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — The graph, deliberately without a model\" title=\"Direct link to Step 3 — The graph, deliberately without a model\" translate=\"no\">​</a></h2>\n<p>No LLM in this step. If something fails now, it is the checkpointer's fault and\nnot the model's — and separating those two is most of what debugging an agent\nconsists of.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-state-object\">The state object<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#the-state-object\" class=\"hash-link\" aria-label=\"Direct link to The state object\" title=\"Direct link to The state object\" translate=\"no\">​</a></h3>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> operator </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> add</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Annotated</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing_extensions </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> TypedDict</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">TypedDict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    completed</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Annotated</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> add</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<p>This declares what the graph remembers. <code>TypedDict</code> gives the shape; the\n<code>Annotated[..., add]</code> part is the interesting half.</p>\n<p>By default, when a node returns a value for a key, that value <strong>replaces</strong> what\nwas there. <code>Annotated[list[str], add]</code> attaches a <strong>reducer</strong> — a function\ncombining the old value with the new one instead of overwriting it. With\n<code>operator.add</code> on a list, that means append.</p>\n<table><thead><tr><th>Declaration</th><th><code>step_one</code> returns <code>[\"a\"]</code>, then <code>step_two</code> returns <code>[\"b\"]</code></th></tr></thead><tbody><tr><td><code>completed: list[str]</code></td><td><code>[\"b\"]</code> — the first result is lost</td></tr><tr><td><code>completed: Annotated[list[str], add]</code></td><td><code>[\"a\", \"b\"]</code> — accumulates</td></tr></tbody></table>\n<p>Get this wrong and your state silently forgets everything except the last node.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-nodes\">The nodes<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#the-nodes\" class=\"hash-link\" aria-label=\"Direct link to The nodes\" title=\"Direct link to The nodes\" translate=\"no\">​</a></h3>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">step_one</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_one running\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">sleep</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"step_one\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">step_two</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_two running — 30s window, KILL ME HERE\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">sleep</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token number\" style=\"color:#36acaa\">30</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"step_two\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>A node is a plain function. It receives the current state and returns a <strong>partial\nupdate</strong> — only the keys it changed, not the whole object. The runtime merges that\nupdate using the reducers you declared.</p>\n<p>The thirty-second sleep in <code>step_two</code> stands in for a slow step — a model call, an\nexternal API, a batch job. It is also what gives you time to kill the process on\ncue in the next step.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"wiring-the-graph\">Wiring the graph<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#wiring-the-graph\" class=\"hash-link\" aria-label=\"Direct link to Wiring the graph\" title=\"Direct link to Wiring the graph\" translate=\"no\">​</a></h3>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> StateGraph</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> name</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> fn </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_one\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> step_one</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_two\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> step_two</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_three\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> step_three</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> fn</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">START</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"step_one\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_one\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"step_two\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_two\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"step_three\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_three\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> END</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p><code>StateGraph(State)</code> binds the schema. <code>add_node</code> registers a function under a\nname — that name is what appears in checkpoints and in traces later. <code>add_edge</code>\ndeclares order, with <code>START</code> and <code>END</code> as the sentinels marking entry and exit.</p>\n<p>This graph is a straight line. Branching comes in part 2.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"attaching-the-checkpointer\">Attaching the checkpointer<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#attaching-the-checkpointer\" class=\"hash-link\" aria-label=\"Direct link to Attaching the checkpointer\" title=\"Direct link to Attaching the checkpointer\" translate=\"no\">​</a></h3>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> PostgresSaver</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">from_conn_string</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">DB_URI</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">setup</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    graph </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token builtin\">compile</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">checkpointer</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    config </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"configurable\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"thread_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> THREAD</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    result </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Four lines, each carrying a trap.</p>\n<p><strong><code>with ... as checkpointer</code>.</strong> <code>from_conn_string</code> is decorated <code>@contextmanager</code>\nin the source — it <em>yields</em>, it does not return. Assigning it directly hands you a\ncontext manager object where you expected a saver, and the error arrives much\nlater than the mistake.</p>\n<p>It also opens the connection with <code>autocommit=True</code>, <code>prepare_threshold=0</code>, and\n<code>row_factory=dict_row</code>. Pass your own connection and you must set those yourself, or\n<code>.setup()</code> can look like it worked and save nothing. Why each is needed is documented\nnowhere — see <a href=\"https://github.com/langchain-ai/langgraph/issues/4937\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">issue #4937</a>,\nclosed without an answer.</p>\n<p><strong><code>.setup()</code></strong> is required on first use. It creates the tables and runs\nmigrations.</p>\n<p><strong><code>compile(checkpointer=...)</code>.</strong> Without this argument the graph runs perfectly and\nsaves nothing. There is no warning. It is the single most likely reason someone\nbelieves checkpointing \"doesn't work\".</p>\n<p><strong><code>thread_id</code>.</strong> The identifier for one conversation or one run. Every checkpoint\nis scoped to it, and resuming means passing the same one. It must stay under 255\ncharacters.</p>\n<p>Together, that is <code>lg_agent.py</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">lg_agent.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> time</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> operator </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> add</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Annotated</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing_extensions </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> TypedDict</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">graph </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> StateGraph</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> START</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> END</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">checkpoint</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">postgres </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> PostgresSaver</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">DB_URI </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"postgresql://langgraph:changeme@127.0.0.1:5434/checkpoints\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">THREAD </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">len</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"ticket-1\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">RESUME </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"--resume\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">TypedDict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    completed</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Annotated</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> add</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">step_one</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_one running\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">sleep</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"step_one\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">step_two</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_two running — 30s window, KILL ME HERE\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">sleep</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token number\" style=\"color:#36acaa\">30</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"step_two\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">step_three</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_three running\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"step_three\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> StateGraph</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> name</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> fn </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_one\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> step_one</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_two\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> step_two</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_three\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> step_three</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> fn</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">START</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"step_one\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_one\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"step_two\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_two\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"step_three\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"step_three\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> END</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> PostgresSaver</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">from_conn_string</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">DB_URI</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">setup</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    graph </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token builtin\">compile</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">checkpointer</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    config </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"configurable\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"thread_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> THREAD</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    snap </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get_state</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"BEFORE  values=</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">snap</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">values</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">  next=</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">snap</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation builtin\">next</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    result </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> RESUME </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"FINAL:\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> result</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    snap </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get_state</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"AFTER   values=</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">snap</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">values</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">  next=</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">snap</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation builtin\">next</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p><code>RESUME</code> is what makes <code>--resume</code> work: on a resume the input is <code>None</code> rather\nthan a fresh state, which is covered in the next step.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python lg_agent.py ticket-41</span><br></div></code></pre></div></div>\n<p>After the first run, four tables exist:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> lg-checkpoints psql </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-U</span><span class=\"token plain\"> langgraph </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> checkpoints </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'\\dt'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> public | checkpoint_blobs      | table | langgraph</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> public | checkpoint_migrations | table | langgraph</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> public | checkpoint_writes     | table | langgraph</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> public | checkpoints           | table | langgraph</span><br></div></code></pre></div></div>\n<table><thead><tr><th>Table</th><th>Holds</th></tr></thead><tbody><tr><td><code>checkpoints</code></td><td>One row per node boundary — the snapshot index</td></tr><tr><td><code>checkpoint_blobs</code></td><td>The serialised state values</td></tr><tr><td><code>checkpoint_writes</code></td><td>Pending writes from nodes that finished in a super-step</td></tr><tr><td><code>checkpoint_migrations</code></td><td>Schema version bookkeeping</td></tr></tbody></table>\n<p><code>checkpoint_writes</code> is the one that makes crash recovery work, as the next step\nshows.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The four checkpoint tables created by setup()\" src=\"https://development-wec.wiline.com/docs/assets/images/lg1-tables-f26befffa3adb092e0cecaa4126737c2.png\" width=\"1520\" height=\"380\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> <code>.setup()</code> writes the schema for you — no manual DDL.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"reading-position\">Reading position<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#reading-position\" class=\"hash-link\" aria-label=\"Direct link to Reading position\" title=\"Direct link to Reading position\" translate=\"no\">​</a></h3>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">snap </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get_state</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"BEFORE  values=</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">snap</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">values</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">  next=</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">snap</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation builtin\">next</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p><code>get_state</code> returns a <code>StateSnapshot</code>. Two fields matter: <code>values</code> is the state\nright now, and <code>next</code> is the tuple of nodes still to run. <code>next=()</code> means the\nthread is finished or has never started; anything else names what is pending.</p>\n<p><code>next</code> is how you answer \"where is this run?\" — the question a loop cannot answer.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--kill-it-and-watch-it-come-back\">Step 4 — Kill it, and watch it come back<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#step-4--kill-it-and-watch-it-come-back\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Kill it, and watch it come back\" title=\"Direct link to Step 4 — Kill it, and watch it come back\" translate=\"no\">​</a></h2>\n<p>Start a fresh thread, then press Ctrl+C during the thirty-second window.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python lg_agent.py ticket-42</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">BEFORE  values={}  next=()</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">step_one running</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">step_two running — 30s window, KILL ME HERE</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">^CKeyboardInterrupt</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"KeyboardInterrupt raised from inside step_two\" src=\"https://development-wec.wiline.com/docs/assets/images/lg1-killed-13e4a54ded6e98bd000ca28160e0eb18.png\" width=\"1594\" height=\"858\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> A real crash — the traceback comes from inside <code>step_two</code>, which therefore never completed.</p>\n<p>Now resume the same <code>thread_id</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python lg_agent.py ticket-42 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--resume</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">BEFORE  values={'completed': ['step_one']}  next=('step_two',)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">step_two running — 30s window, KILL ME HERE</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">step_three running</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FINAL: {'completed': ['step_one', 'step_two', 'step_three']}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AFTER   values={'completed': [...]}  next=()</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The resumed run showing recovered state and next=(&amp;#39;step_two&amp;#39;,)\" src=\"https://development-wec.wiline.com/docs/assets/images/lg1-resumed-060c0f536dc33b295570816403af58d0.png\" width=\"1220\" height=\"266\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 4.</strong> The <code>BEFORE</code> line is the proof — state recovered from Postgres by a brand-new process.</p>\n<p>Read that first line closely, because it is the whole tutorial:</p>\n<ul>\n<li class=\"\"><code>values</code> shows <code>step_one</code>'s result, recovered from Postgres by a process that\nhad not existed when it was produced</li>\n<li class=\"\"><code>next=('step_two',)</code> — the graph knows exactly which node did not finish</li>\n<li class=\"\"><strong><code>step_one</code> did not run again.</strong> It executed once across three invocations</li>\n</ul>\n<p>That last point is what <code>checkpoint_writes</code> buys: LangGraph keeps the completed\nwrites from a super-step, so resuming does not repeat work that succeeded.</p>\n<p>The resume call itself is:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p><code>None</code> as input means \"no new input — continue from the checkpoint.\" Worth\nknowing: this is documented on the interrupts page for static breakpoints, but not\non the persistence or durable-execution pages, which is where you look when your\nprocess has just died. It works; it is simply not written down where you need it.</p>\n<p>Kill it a second time and resume again. It works, because reading a checkpoint\ndoes not consume it.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--pause-for-a-human\">Step 5 — Pause for a human<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#step-5--pause-for-a-human\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — Pause for a human\" title=\"Direct link to Step 5 — Pause for a human\" translate=\"no\">​</a></h2>\n<p>A crash is an accidental stop. An approval is a deliberate one — and once you can\nsurvive the first, the second is nearly free.</p>\n<p>The agent is about to write an appointment into a customer's calendar. That is\nirreversible and a real person sees it, so you want someone to check first.</p>\n<p>Without durable state, waiting for a human means holding a process open. With state\nin Postgres the agent stops, the process exits, and the approval arrives whenever it\narrives — ten minutes or Monday morning.</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">types </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> interrupt</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> Command</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">approval</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    decision </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> interrupt</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"question\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Approve step_two?\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"so_far\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> state</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"approval\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"approved\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> decision</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>Save it as <code>lg_agent2.py</code> and run it:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python lg_agent2.py booking-7</span><br></div></code></pre></div></div>\n<p><code>interrupt()</code> does two things at once: it stops the graph, and it hands its\nargument out to whoever is calling. That argument is your question to the human —\nany JSON-serialisable value.</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">step_one running</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">RESULT: {'completed': ['step_one'], 'approved': '',</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">         '__interrupt__': [Interrupt(value={'question': 'Approve step_two?',</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">         'so_far': ['step_one']}, id='29f211cf5a73ea78e0f694e1bd38822b')]}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AFTER   next=('approval',)  interrupts=(Interrupt(...),)</span><br></div></code></pre></div></div>\n<p>No exception, exit code 0. Note the difference from Step 4: the crash produced a\ntraceback, this produces a clean return.</p>\n<p>The pause surfaces in two places, and the difference matters:</p>\n<table><thead><tr><th>Where</th><th>Use</th></tr></thead><tbody><tr><td><code>__interrupt__</code> in the invoke result</td><td>The caller that just ran the graph</td></tr><tr><td><code>snapshot.interrupts</code></td><td>Any other process — a dashboard rendering the question</td></tr></tbody></table>\n<p>The <code>id</code> correlates an answer back to the right pause, which matters once more\nthan one approval is outstanding.</p>\n<p>The process is now gone; the state is in Postgres. An hour or a week later:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python lg_agent2.py booking-7 </span><span class=\"token function\" style=\"color:#d73a49\">yes</span><br></div></code></pre></div></div>\n<p>which calls:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">Command</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">resume</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"yes\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">approval resumed with: yes</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">step_two running (approved=yes)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">AFTER   next=()  interrupts=()</span><br></div></code></pre></div></div>\n<p><code>Command(resume=...)</code> is different from <code>invoke(None, ...)</code>. <code>None</code> says \"carry\non\"; <code>Command(resume=X)</code> says \"carry on, and <code>interrupt()</code> should return <code>X</code>.\"\nThat value came from the command line, through <code>interrupt()</code>'s return, into state,\nand was read by the next node.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The graph pausing at the interrupt, then resuming with the approval value\" src=\"https://development-wec.wiline.com/docs/assets/images/lg1-interrupt-ea585f38dd6726bebaeeff4db224460a.png\" width=\"2214\" height=\"488\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 5.</strong> Pause and resume — two separate processes, one run.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-part-that-will-bite-you\">The part that will bite you<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#the-part-that-will-bite-you\" class=\"hash-link\" aria-label=\"Direct link to The part that will bite you\" title=\"Direct link to The part that will bite you\" translate=\"no\">​</a></h3>\n<p>On resume, LangGraph re-executes the interrupted node <strong>from the top</strong>. This time\n<code>interrupt()</code> returns your value instead of pausing — but everything above it in\nthat function has already run a second time.</p>\n<p>Put a print before the interrupt, then run a fresh thread and approve it:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python lg_agent2.py booking-8</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python lg_agent2.py booking-8 approve</span><br></div></code></pre></div></div>\n<p>It appears in both runs, for a single approval:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">run 1:  step_one running</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        SIDE EFFECT — before interrupt</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        RESULT: {... '__interrupt__': [Interrupt(...)]}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">run 2:  SIDE EFFECT — before interrupt</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        approval resumed with: approve</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        step_two running (approved=approve)</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The same side-effect line printed in both the pausing run and the resuming run\" src=\"https://development-wec.wiline.com/docs/assets/images/lg1-double-side-effect-89c6352b4d9d897ec50519025547fca8.png\" width=\"2214\" height=\"550\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 6.</strong> One approval, two executions of everything above the interrupt.</p>\n<p>If that line were <code>send_email()</code> or <code>charge_card()</code>, one approval charges the\ncustomer twice. Nothing raises. Final state is correct. Only the side effect\nduplicated — which is why this is easy to ship and hard to notice.</p>\n<p><strong>Put nothing before <code>interrupt()</code> in that node.</strong> Move side effects after it, or\ngive the interrupt a node of its own that does nothing else. The same reasoning\napplies to crash resume: a node can always run more than once, so anything with an\nexternal consequence should be idempotent or isolated.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-6--a-real-model-and-traces\">Step 6 — A real model, and traces<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#step-6--a-real-model-and-traces\" class=\"hash-link\" aria-label=\"Direct link to Step 6 — A real model, and traces\" title=\"Direct link to Step 6 — A real model, and traces\" translate=\"no\">​</a></h2>\n<p>Two more packages: one to talk to the model, one to trace it.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">pip </span><span class=\"token function\" style=\"color:#d73a49\">install</span><span class=\"token plain\"> langchain-openai langfuse</span><br></div></code></pre></div></div>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">llm </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ChatOpenAI</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"Qwen2.5-3B-Instruct\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    base_url</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"https://inference.wiline.com/v1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"WEC_API_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    temperature</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p><code>ChatOpenAI</code> is not OpenAI-specific — it speaks the OpenAI wire protocol, which\nWEC Inference implements. Pointing <code>base_url</code> at it is the whole integration.\n<code>temperature=0</code> keeps runs comparable while you are testing.</p>\n<p>The call goes inside a node like any other work:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">write</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> llm</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Write one sentence about </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">state</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'topic'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">.\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"write\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"draft\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>You need a Langfuse project and an API key pair for this — created under\n<strong>Organization → Project</strong>, then <strong>Settings → API Keys</strong>, as covered in\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/\">Observe production with Langfuse</a>.</p>\n<p>Tracing attaches per invocation, in the same config dict as the thread id:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langfuse</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">langchain </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> CallbackHandler</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">handler </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> CallbackHandler</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">config </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"configurable\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"thread_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> THREAD</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"callbacks\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">handler</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><code>configurable</code> is LangGraph's own settings; <code>callbacks</code> is LangChain's observer\nhook. Langfuse subscribes to node start and end events and builds spans from them\n— which is why the trace mirrors your graph without any extra instrumentation.</p>\n<p>Credentials come from the environment — in SDK v4 <code>CallbackHandler()</code> takes no\narguments, so this is the only way to configure it:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">WEC_API_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">'...'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">LANGFUSE_PUBLIC_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">'pk-lf-...'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">LANGFUSE_SECRET_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">'sk-lf-...'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">LANGFUSE_HOST</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">'http://localhost:3001'</span><br></div></code></pre></div></div>\n<p>Putting it together as <code>lg_llm.py</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> sys</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> operator </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> add</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Annotated</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing_extensions </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> TypedDict</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">graph </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> StateGraph</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> START</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> END</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langgraph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">checkpoint</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">postgres </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> PostgresSaver</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langchain_openai </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> ChatOpenAI</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langfuse</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">langchain </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> CallbackHandler</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">DB_URI </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"postgresql://langgraph:changeme@127.0.0.1:5434/checkpoints\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">THREAD </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">len</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sys</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">argv</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"draft-1\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">llm </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ChatOpenAI</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"Qwen2.5-3B-Instruct\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    base_url</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"https://inference.wiline.com/v1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"WEC_API_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    temperature</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">TypedDict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    topic</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    completed</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Annotated</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> add</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    draft</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">plan</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"plan running\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"plan\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">write</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> llm</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Write one sentence about </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">state</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'topic'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">.\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"model said:\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"write\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"draft\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> StateGraph</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">State</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"plan\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> plan</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_node</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"write\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> write</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">START</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"plan\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"plan\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"write\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">add_edge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"write\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> END</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">handler </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> CallbackHandler</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> PostgresSaver</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">from_conn_string</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">DB_URI</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">setup</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    graph </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> builder</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token builtin\">compile</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">checkpointer</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">checkpointer</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    config </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"configurable\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"thread_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> THREAD</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"callbacks\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">handler</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    result </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> graph</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">invoke</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"topic\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"edge computing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"completed\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"draft\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> config</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"FINAL:\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> result</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python lg_llm.py draft-1</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">LangGraph            2.18s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  plan               0.00s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  write              2.14s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    ChatOpenAI       2.13s   36 -&gt; 37 (73 tokens)</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Langfuse trace with per-node latency and token counts\" src=\"https://development-wec.wiline.com/docs/assets/images/lg1-langfuse-trace-8385ab962b94706bb03e2df32a87a0a6.png\" width=\"2526\" height=\"1426\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 7.</strong> Span tree, graph topology and token counts — from one callback.</p>\n<p>Almost all of it is the model call. That leaves about 40ms for the graph and\ntracing, and the gap stayed near 40ms even on runs that took twice as long. <code>plan</code>,\nwhich calls nothing, reports 0.00s.</p>\n<p>The useful conclusion: <strong>orchestration is not your latency problem.</strong> If an agent\nfeels slow, the graph is not why.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting-real-errors\">Troubleshooting (real errors)<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#troubleshooting-real-errors\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting (real errors)\" title=\"Direct link to Troubleshooting (real errors)\" translate=\"no\">​</a></h2>\n<p><strong><code>address already in use</code> vs <code>port is already allocated</code>.</strong> These look\ninterchangeable and are not. The first means a <em>host process</em> holds the port; the\nsecond means another <em>container</em> does — Docker's own bookkeeping, before the bind\nis attempted. Diagnose with:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--format</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{{.Names}}\\t{{.Ports}}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> ss </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-ltnp</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> :543</span><br></div></code></pre></div></div>\n<p><strong>A failed <code>docker run</code> blocks the name.</strong> The container is created, then dies, and\nstill holds its name — so your retry fails differently than the first attempt did.\n<code>docker rm &lt;name&gt;</code> before retrying.</p>\n<p><strong><code>ModuleNotFoundError: No module named 'langchain'</code></strong> when importing Langfuse's\n<code>CallbackHandler</code>. Neither LangGraph nor <code>langchain-openai</code> needs the <code>langchain</code>\numbrella package; Langfuse imports it purely as a version sentinel. The fix is\ninstalling a package nothing else in your stack uses.</p>\n<p>Two upstream threads are worth knowing apart.\n<a href=\"https://github.com/langfuse/langfuse/issues/9758\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">#9758</a> reported the import\nfailure in October 2025 and is <strong>closed</strong> — but it still reproduces on the SDK\nversion above, so read it as history rather than as a fix that landed.\n<a href=\"https://github.com/langfuse/langfuse/issues/13651\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">#13651</a> is <strong>open</strong> and\nproposes the actual remedy: read <code>langchain_core.__version__</code> instead of requiring\nthe umbrella package at all.</p>\n<p><strong>Langfuse disables itself in silence.</strong> Miss an environment variable and you get\none warning line, then a completely normal run — exit code 0, correct output, no\ntraces at all:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Authentication error: Langfuse client initialized without public_key.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Client will be disabled.</span><br></div></code></pre></div></div>\n<p>In SDK v4 <code>CallbackHandler()</code> accepts no constructor arguments, so environment\nvariables are the only configuration path — and a missing one does not raise.\nAssert the client is enabled at startup rather than trusting the absence of errors.</p>\n<p><strong>Version drift.</strong> Most LangGraph + Langfuse material targets Langfuse SDK v3. On\nv4 the <code>update_trace</code> parameter is gone and raises <code>TypeError</code>, and credentials can\nno longer be passed to the constructor.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>State that survives a restart</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>Part 2: handing work between agents — and what happens to shared state when they\ndisagree.</p>\n<p>Related reading on this site:</p>\n<ul>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/\">Component-level tracing for agent tool calls</a> — finding which step of an agent failed, not just that it did</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/\">Self-host the Hermes Agent with persistent memory</a> — a different persistence model, memory across conversations rather than state within a run</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/news/loop-engineering-graph-engineering-what-survives/\">\"Loop Engineering Is Dead\"</a> — why the industry moved from loops to graphs</li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/langgraph-stateful-agent-wec-instance/",
            "title": "Checkpoint a LangGraph agent on a WEC Instance so crashes cost you nothing",
            "summary": "Persist agent state to Postgres on a WEC Instance, call a model on the WEC Inference API, then kill the process mid-run twice and resume both times from exactly where it stopped — plus the human-approval pause, and the interrupt that silently fires your side effects twice.",
            "date_modified": "2026-08-26T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "agents",
                "langgraph",
                "state",
                "postgres",
                "langfuse",
                "self-hosting",
                "wec",
                "wec-inference"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__presidio\" src=\"data:image/webp;base64,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\" alt=\"Presidio\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2255\" height=\"527\" fill=\"none\" viewBox=\"0 0 2255 527\" class=\"tutorialHero__langfuse\"><path fill=\"#1B1917\" d=\"M652.669 116.433c0-10.261-7.683-17.956-17.926-17.956H607V60h87.923v316.366h-42.254zM804.056 379.786c-39.693 0-72.131-27.362-72.131-68.831 0-41.042 28.596-69.686 84.935-69.686h39.693c7.256 0 12.805-5.558 12.805-12.826v-8.55c0-25.651-20.487-38.905-40.547-38.905-18.353 0-33.291 8.123-41.828 26.934h-45.668c11.95-44.462 46.095-65.41 88.349-65.41 40.547 0 82.374 22.231 82.374 77.808v156.046h-42.68v-14.109c0-5.13-5.549-7.695-9.817-4.702-16.646 11.97-32.438 22.231-55.485 22.231m5.975-38.477c18.78 0 34.572-9.406 52.498-27.362 4.695-4.702 6.829-10.688 6.829-17.1v-6.413c0-7.268-5.549-12.826-12.805-12.826H815.58c-27.743 0-40.974 12.398-40.974 31.637 0 17.956 12.377 32.064 35.425 32.064M961.855 376.366V147.642h42.255v19.666c0 5.13 6.4 6.84 10.24 2.992 13.23-12.825 31.59-27.788 60.61-27.788 36.71 0 70.85 23.086 70.85 75.671v158.183h-42.25V227.161c0-29.499-17.93-45.745-39.7-45.745-20.91 0-35 11.971-49.93 29.499-7.26 8.978-9.82 19.238-9.82 30.354v135.097zM1287.48 467c-52.5 0-85.79-24.796-95.61-63.273h46.1c7.25 15.818 20.06 25.651 45.67 25.651 34.14 0 55.48-20.093 55.48-65.838v-7.268c0-5.13-4.27-7.695-9.39-3.42-14.08 12.398-32.01 20.093-49.08 20.093-58.05 0-96.46-44.889-96.46-115.003 0-70.113 44.39-115.43 98.17-115.43 15.79 0 30.3 4.275 44.38 14.963 5.98 4.275 12.38.855 12.38-5.986v-3.847h42.26V363.54c0 72.678-43.97 103.46-93.9 103.46m-2.56-132.959c19.2 0 33.29-8.55 44.81-20.949 7.26-8.122 9.39-13.68 9.39-26.506v-61.563c0-12.826-2.13-20.948-10.67-29.071-9.39-8.978-22.62-14.964-39.69-14.964-35 0-61.89 29.072-61.89 76.954 0 47.883 24.76 76.099 58.05 76.099M1455.92 199.372c0-7.268-5.97-13.253-13.23-13.253h-32.44v-38.477h32.44c7.26 0 13.23-5.985 13.23-13.253v-5.986c0-45.744 23.48-68.403 69.15-68.403h29.02v38.477h-29.45c-17.5 0-26.46 9.833-26.46 29.926v5.986c0 7.268 5.97 13.253 13.23 13.253h42.68v38.477h-42.68c-7.26 0-13.23 5.985-13.23 13.253v176.994h-42.26zM1652.02 381.496c-35.85 0-69.14-23.086-69.14-75.671V147.642h42.25v150.06c0 29.499 17.07 44.889 37.13 44.889 21.77 0 35.85-11.97 50.79-29.499 7.26-8.977 9.82-19.238 9.82-30.354V147.642h42.25v228.724h-42.25V356.7c0-5.131-6.4-6.841-10.24-2.993-13.24 12.826-31.59 27.789-60.61 27.789M1893.57 381.496c-38.84 0-79.39-19.239-90.06-65.838h43.54c6.4 17.528 23.9 29.498 44.81 29.498 23.05 0 37.13-13.68 37.13-30.353 0-16.246-11.09-25.224-28.59-30.354l-36.28-10.261c-31.58-8.978-55.06-29.499-55.06-64.556 0-38.049 35.43-67.12 75.55-67.12 32.01 0 70.85 14.535 81.09 65.41h-40.55c-5.55-17.528-20.06-29.071-40.54-29.071-20.06 0-34.58 12.398-34.58 28.216 0 13.253 8.11 23.086 27.32 28.644l34.15 9.833c32.43 9.406 58.47 29.072 58.47 66.693 0 39.332-34.15 69.259-76.4 69.259M2098.54 381.496c-61.46 0-102.01-51.73-102.01-119.706s43.11-119.278 101.58-119.278c63.6 0 96.89 51.302 96.89 109.872v23.087h-144.26c-5.98 0-8.54 3.847-7.26 13.68 4.7 32.064 30.31 54.295 55.49 54.295 18.78 0 35-9.405 45.67-27.788h44.81c-16.22 40.187-49.94 65.838-90.91 65.838m43.11-141.51c6.83 0 9.39-3.42 7.68-14.108-4.69-26.506-24.33-45.317-51.22-45.317-25.6 0-47.37 18.811-54.2 45.745-2.56 9.833.85 13.68 6.83 13.68z\"></path><path fill=\"#FF5D5F\" d=\"m286.292 286.105 34.597 27.791s26.473-19.661 45.941-22.545c20.418-3.025 42.202 8.359 62.388 21.93 30.489 20.498 56.149 46.508 56.149 46.508l30.06-29.493s-82.879-89.795-148.597-81.672c-43.105 5.328-80.538 37.481-80.538 37.481\"></path><path fill=\"#4E9CFF\" d=\"M88.358 114.862 60 146.056s79.009 73.732 141.224 73.732c28.358 0 67.684-22.216 101.523-51.079 19.283-16.448 40.835-35.13 62.388-35.13 14.487 0 33.594 7.673 51.612 27.824 0 0 11.63-6.974 18.716-11.985 6.228-4.404 15.479-11.91 15.479-11.91-25.918-27.663-63.407-47.883-85.807-45.9-36.299.005-62.388 22.601-94.717 48.735s-45.94 36.907-69.194 36.907c-39.134 0-112.866-62.388-112.866-62.388M88.358 352.463 60 321.269s79.009-73.732 141.224-73.732c28.358 0 67.684 22.216 101.523 51.079 19.283 16.448 40.835 35.13 62.388 35.13 14.556 0 33.518-7.989 51.612-28.358 0 0 10.877 6.705 17.582 11.344 6.894 4.769 17.015 12.655 17.015 12.655-25.931 27.883-63.693 48.323-86.209 46.33-36.299-.005-57.851-19.24-90.179-45.374-32.329-26.133-50.478-40.268-73.732-40.268-39.134 0-112.866 62.388-112.866 62.388M458.142 185.149c-7.378 5.1-19.283 12.478-19.283 12.478s6.806 14.746 6.806 34.597-6.239 36.866-6.239 36.866 10.688 6.675 17.582 11.343c7.162 4.849 18.149 13.045 18.149 13.045s13.045-27.224 13.045-61.254-13.045-59.552-13.045-59.552-10.236 7.792-17.015 12.477\"></path><path fill=\"#FF5D5F\" d=\"m287.995 180.612 32.895-27.224s26.473 19.046 45.941 21.93c20.417 3.026 42.202-8.359 62.388-21.93 30.489-20.498 56.149-46.507 56.149-46.507l30.06 29.492s-82.879 89.795-148.597 81.672c-43.105-5.328-78.836-37.433-78.836-37.433M208.601 91c42.538 0 78.264 36.299 78.264 36.299s-9.941 7.832-16.448 13.045c-6.777 5.429-17.582 14.179-17.582 14.179s-18.711-19.851-44.234-19.851c-10.465 0-24.066 6.286-38.567 18.716-11.188 9.591-22.829 21.514-30.627 36.299-6.743 12.784-10.42 27.85-10.776 43.672-.446 19.873 6.597 40.704 18.149 57.283 7.743 11.112 16.983 19.474 26.657 26.657 12.555 9.322 25.648 15.881 35.164 15.881 10.166 0 19.306-3.533 26.09-6.806 10.776-6.239 19.278-13.612 19.278-13.612l33.463 27.791s-13.612 13.612-32.323 23.821c-12.091 5.963-27.632 11.91-46.508 11.91-18.862 0-40.767-10.022-61.254-25.522-13.244-10.021-26.225-21.895-36.298-36.299-16.51-23.607-25.017-52.328-24.96-81.104.057-29.136 9.451-57.993 26.094-81.672C138.273 117.657 176.86 91 208.601 91\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 5 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->5</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->5<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting an LLM gateway</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">One endpoint, scoped keys</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Route work to the right model</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Mask PII at the gateway</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">4</span><span class=\"skillTracker__skill\" data-state=\"current\">Clean traces, untouched answers</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove it holds under load</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/\">Part 3</a> configured Presidio to mask\nprompts at logging time and deliberately stopped short of proving it. Nothing had\nbeen wired to a logging destination yet, so there was no trace to inspect.</p>\n<p>This post wires one up, finds <code>&lt;PERSON&gt;</code> where it should be — and then finds the\ncustomer's real email address sitting a few lines below it, in the model's reply.</p>\n<!-- -->\n<!-- -->\n<p>Everything in this post follows from where those two hooks sit. <code>post_call</code> fires\n<em>before</em> the arrow back to the caller, so masking there rewrites the answer they\nreceive. <code>async_logging_hook</code> fires <em>after</em> it, on the branch to the logger only.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"sending-the-gateways-traffic-to-langfuse\">Sending the gateway's traffic to Langfuse<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/#sending-the-gateways-traffic-to-langfuse\" class=\"hash-link\" aria-label=\"Direct link to Sending the gateway's traffic to Langfuse\" title=\"Direct link to Sending the gateway's traffic to Langfuse\" translate=\"no\">​</a></h2>\n<p>Langfuse is already running in this series; if you followed\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/\">the observability tutorial</a> you\nhave an instance. Create a project for the gateway, then take its two keys from\n<strong>Settings → API Keys</strong>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Langfuse project setup screen showing the secret and public keys\" src=\"https://development-wec.wiline.com/docs/assets/images/gw4-langfuse-keys-0868edc9108438174e185d84a9801382.png\" width=\"1905\" height=\"992\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 1.</strong> A separate project keeps gateway traffic away from whatever else\nyou are tracing.</p>\n<p>Add three variables to the gateway's <code>.env</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">LANGFUSE_PUBLIC_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">pk-lf-</span><span class=\"token punctuation\" style=\"color:#393A34\">..</span><span class=\"token plain\">.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">LANGFUSE_SECRET_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">sk-lf-</span><span class=\"token punctuation\" style=\"color:#393A34\">..</span><span class=\"token plain\">.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">LANGFUSE_HOST</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">http://your-host:3001</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>The variable name is not the one Langfuse gives you</div><div class=\"admonitionContent_BuS1\"><p>Langfuse's own setup screen prints a <code>.env</code> block containing\n<code>LANGFUSE_BASE_URL</code>. The gateway does not read that. It reads:</p><div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"LANGFUSE_HOST\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"https://cloud.langfuse.com\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div><p>Copy their snippet verbatim and your host is ignored in favour of the public\ncloud endpoint — where your self-hosted keys will not authenticate. Traces go\nnowhere, and nothing errors.</p></div></div>\n<p>Then turn the callback on:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_settings</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">drop_params</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:#36acaa\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">success_callback</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"langfuse\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">failure_callback</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"langfuse\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"restarting-is-not-enough\">Restarting is not enough<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/#restarting-is-not-enough\" class=\"hash-link\" aria-label=\"Direct link to Restarting is not enough\" title=\"Direct link to Restarting is not enough\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose restart litellm</span><br></div></code></pre></div></div>\n<p>Send a request after that and the logs say:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Langfuse client is disabled since no public_key was provided as a parameter</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">or environment variable 'LANGFUSE_PUBLIC_KEY'.</span><br></div></code></pre></div></div>\n<p>The keys are in <code>.env</code>. They are not in the container:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway </span><span class=\"token function\" style=\"color:#d73a49\">printenv</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> LANGFUSE</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">0</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span><code>restart</code> does not reload <code>env_file</code></div><div class=\"admonitionContent_BuS1\"><p><code>docker compose restart</code> restarts the <em>process</em> inside the container that\nalready exists, with the environment it was created with. New variables in\n<code>.env</code> are not picked up.</p><p>The <code>config.yaml</code> change <em>did</em> take effect, because that is a bind-mounted file\nread at startup. So the gateway looks correctly configured — callbacks\ninitialised, no errors — while having no credentials at all.</p><p>Use <code>docker compose up -d</code>, which recreates the container.</p></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway </span><span class=\"token function\" style=\"color:#d73a49\">printenv</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> LANGFUSE</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">3</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"printenv inside the container now reporting three LANGFUSE variables\" src=\"https://development-wec.wiline.com/docs/assets/images/gw4-recreate-env-8aff004f8b60f6c931690f76127937bf.png\" width=\"363\" height=\"71\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 2.</strong> After <code>up -d</code>, the variables are in the container.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-trace-arrives-and-the-loop-closes\">The trace arrives, and the loop closes<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/#the-trace-arrives-and-the-loop-closes\" class=\"hash-link\" aria-label=\"Direct link to The trace arrives, and the loop closes\" title=\"Direct link to The trace arrives, and the loop closes\" translate=\"no\">​</a></h2>\n<p>Wait for <code>Application startup complete</code>, then send a prompt carrying a name and\nan address:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-small\",\"messages\":[{\"role\":\"user\",\"content\":\"What is the first name of Maria Alvarez, and what is the domain of maria.alvarez@example.com? Answer in one short line.\"}],\"max_tokens\":40}'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.choices[0].message.content'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">The first name of Maria Alvarez is Maria; the domain of maria.alvarez@example.com is example.com.</span><br></div></code></pre></div></div>\n<p>That answer is the control. The model could not have produced \"Maria\" from a\nplaceholder, so the live request reached it unmasked — exactly what\n<code>logging_only</code> promises.</p>\n<p>Now the trace:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"A Langfuse trace showing the masked input above the unmasked assistant reply\" src=\"https://development-wec.wiline.com/docs/assets/images/gw4-trace-input-masked-f167939e2680fdb4984817374cb86a8e.png\" width=\"1899\" height=\"989\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 3.</strong> Input masked. Output not.</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Stored input</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">What is the first name of &lt;PERSON&gt;, and what is the domain of &lt;EMAIL_ADDRESS&gt;?</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Stored output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">The first name of Maria Alvarez is Maria; the domain of maria.alvarez@example.com is example.com.</span><br></div></code></pre></div></div>\n<p>Part 3's job is done — the prompt is masked in the log while the model saw the\nreal text. And the PII is in the trace anyway, because the model repeated it\nback.</p>\n<p>That is not an edge case. Repeating the customer's name is what a support\nassistant is <em>for</em>.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-spend-log-is-worse\">The spend log is worse<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/#the-spend-log-is-worse\" class=\"hash-link\" aria-label=\"Direct link to The spend log is worse\" title=\"Direct link to The spend log is worse\" translate=\"no\">​</a></h2>\n<p>The gateway's own database stores the same request differently again:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/spend/logs </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'.[0] | {messages, response}'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">\"messages\": {}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">\"response\": { ... \"content\": \"The first name of Maria Alvarez is Maria;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">              the domain of maria.alvarez@example.com is example.com.\" ... }</span><br></div></code></pre></div></div>\n<p>No prompt at all, and the completion in full. Two stores, two shapes, the same\nhole in both.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"widening-the-scope-trades-one-problem-for-another\">Widening the scope trades one problem for another<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/#widening-the-scope-trades-one-problem-for-another\" class=\"hash-link\" aria-label=\"Direct link to Widening the scope trades one problem for another\" title=\"Direct link to Widening the scope trades one problem for another\" translate=\"no\">​</a></h2>\n<p>Part 3 set <code>presidio_filter_scope: input</code>. The obvious fix is to scan both\ndirections:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio_filter_scope</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> both</span><br></div></code></pre></div></div>\n<p>Restart, send the identical request, and read the response the caller gets:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">The first name of &lt;PERSON&gt; is &lt;PERSON&gt;; the domain of &lt;EMAIL_ADDRESS&gt; is &lt;URL&gt;.</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The curl response containing placeholders instead of the real name and address\" src=\"https://development-wec.wiline.com/docs/assets/images/gw4-response-masked-5e5f2e3cd767089449d5067827a9f518.png\" width=\"390\" height=\"192\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 4.</strong> The caller's own terminal, not a log viewer.</p>\n<p>That is not the log. That is the answer returned to the client.</p>\n<p><code>mode: logging_only</code> is still set. It did not help, because it never reaches the\noutput path — the documentation for the scope setting says so directly:</p>\n<blockquote>\n<p>Use <code>presidio_filter_scope: output</code> (or <code>both</code>) when you want Presidio to\nactively scan and mask the model's response <strong>before it reaches the user</strong>.</p>\n</blockquote>\n<p>Masking the live response is the documented purpose of an output scope. What is\nundocumented is the combination: nothing describes what happens when you set\n<code>logging_only</code> <em>and</em> an output scope, and the code resolves it in favour of the\nlive path.</p>\n<p>In <code>guardrail_initializers.py</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> run_output</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    output_callback </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> _make_presidio_callback</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        apply_to_output</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        event_hook</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">GuardrailEventHooks</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">post_call</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">value</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># hard-coded</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        output_parse_pii</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">False</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>The <code>mode</code> you configured is passed to the <em>input</em> callback. The output callback\ngets <code>post_call</code> regardless.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Check the model still receives real data</div><div class=\"admonitionContent_BuS1\"><p>A masked reply has two possible causes: the response was masked on the way out,\nor the model received placeholders and answered honestly about them. They look\nidentical.</p><p>Ask for something that encodes the real value without containing it — a single\nletter is not PII, so masking cannot hide it:</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">..</span><span class=\"token plain\">. </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-small\",\"messages\":[{\"role\":\"user\",\"content\":\"Reply with ONLY the first letter of the first name of Maria Alvarez.\"}],\"max_tokens\":200}'</span><br></div></code></pre></div></div><div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Mª</span><br></div></code></pre></div></div><p><code>M</code>. The model had the real name. Only the response was rewritten.</p></div></div>\n<p>So the two settings give you a choice, and neither is the one you want:</p>\n<table><thead><tr><th></th><th>Logged input</th><th>Logged output</th><th>Caller's response</th></tr></thead><tbody><tr><td><code>filter_scope: input</code></td><td>masked</td><td><strong>raw PII</strong></td><td>untouched</td></tr><tr><td><code>filter_scope: both</code></td><td>masked</td><td>masked</td><td><strong>masked</strong></td></tr></tbody></table>\n<p>This has been reported.\n<a href=\"https://github.com/BerriAI/litellm/issues/30447\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Issue #30447</a> — <em>\"logging_only\nPresidio guardrail corrupts user-facing response\"</em> — was filed on 15 June 2026\nand closed the next day, with no discussion, alongside a pull request titled\n<em>\"fix(presidio): don't mask the live request when guardrail is logging_only\"</em>.\nThe report was about the response. The fix addressed the request.\n<a href=\"https://github.com/BerriAI/litellm/issues/35951\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">#35951</a> is still open on a\nrelated path.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"doing-it-at-logging-time-instead\">Doing it at logging time instead<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/#doing-it-at-logging-time-instead\" class=\"hash-link\" aria-label=\"Direct link to Doing it at logging time instead\" title=\"Direct link to Doing it at logging time instead\" translate=\"no\">​</a></h2>\n<p>The gap exists because <code>post_call</code> runs while the response is still travelling\nto the caller. There is a later hook that does not.</p>\n<p><code>async_logging_hook</code> fires after the model has answered and before the loggers\nrun. It receives the request and the result, and <strong>whatever it returns is what\ngets logged</strong>. The caller already has their response by then.</p>\n<p>That hook is documented, under\n<a href=\"https://docs.litellm.ai/docs/observability/scrub_data\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Scrub Logged Data</a> — but\nthe example there is a placeholder that replaces every message with the literal\nstring <code>MASK_THIS_ASYNC_VALUE</code>. It shows the hook exists; it does not mask\nanything, it touches only <code>kwargs[\"messages\"]</code>, and it mutates in place. What\nfollows uses the same extension point and adds the parts that make it work:\nreal Presidio calls, all three places PII hides, and a copy so the caller's\nresponse survives.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Their example does not run as written</div><div class=\"admonitionContent_BuS1\"><p>The snippet on that page ends <code>return kwargs, responses</code>, but the parameter is\nnamed <code>result</code>. <code>responses</code> is undefined, so copying it verbatim raises\n<code>NameError</code>.</p></div></div>\n<p>Create <code>scrubber.py</code> next to <code>config.yaml</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">scrubber.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> copy</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> typing </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> Any</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> Tuple</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> httpx</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> litellm</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">integrations</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">custom_logger </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> CustomLogger</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ANALYZER </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"PRESIDIO_ANALYZER_API_BASE\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://presidio-analyzer:3000\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ANONYMIZER </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"PRESIDIO_ANONYMIZER_API_BASE\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://presidio-anonymizer:3000\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">_mask</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">text</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> text </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> text</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">strip</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> text</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> httpx</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">AsyncClient</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">timeout</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">10.0</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">post</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">ANALYZER</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">/analyze\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> json</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"text\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> text</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"language\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"en\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        found </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> found</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> text</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">post</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">ANONYMIZER</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">/anonymize\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            json</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"text\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> text</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"analyzer_results\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> found</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"text\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">PresidioLogScrubber</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">CustomLogger</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">async_logging_hook</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        self</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> kwargs</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> result</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Any</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> call_type</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Tuple</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> Any</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> call_type </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"completion\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"acompletion\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> kwargs</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> result</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        kwargs </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> copy</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">deepcopy</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">kwargs</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> message </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> kwargs</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">isinstance</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                message</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> _mask</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        slo </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> kwargs</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"standard_logging_object\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">isinstance</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">slo</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> message </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> slo</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">isinstance</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">and</span><span class=\"token plain\"> </span><span class=\"token builtin\">isinstance</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                    message</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> _mask</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            response </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> slo</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"response\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">isinstance</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">response</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\">dict</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> choice </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> response</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"choices\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                    message </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">choice </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"message\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token builtin\">isinstance</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                        message</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> _mask</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># `result` is the object the caller already holds. Copy before masking.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        logged_result </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> copy</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">deepcopy</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">result</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> choice </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token builtin\">getattr</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">logged_result</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"choices\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            message </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">getattr</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">choice</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"message\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> message </span><span class=\"token keyword\" style=\"color:#00009f\">is</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">and</span><span class=\"token plain\"> </span><span class=\"token builtin\">isinstance</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token builtin\">getattr</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                message</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> _mask</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> kwargs</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> logged_result</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">instance </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> PresidioLogScrubber</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Three details carry the whole thing.</p>\n<p><strong>PII hides in three places, not one.</strong> <code>kwargs[\"messages\"]</code> is the prompt.\n<code>kwargs[\"standard_logging_object\"]</code> is what the logging integrations actually\nread — this is the subject of issue #35951. And <code>result</code> holds the completion.\nMiss any one and something leaks.</p>\n<p><strong><code>result</code> is the caller's object.</strong> Mutating it in place reproduces the bug we\nare working around. It gets deep-copied first, and the copy is what we mask and\nreturn.</p>\n<p><strong><code>_mask</code> is async on purpose.</strong> More on that below, because getting it wrong\ncosts measurable latency.</p>\n<p>Mount the file and register it. It must sit beside <code>config.yaml</code>, because\n<code>get_instance_fn</code> resolves the dotted path relative to the config file's\ndirectory:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.yml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">volumes</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./config.yaml</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/config.yaml</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./scrubber.py</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/scrubber.py</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div></code></pre></div></div>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_settings</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">drop_params</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:#36acaa\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">callbacks</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"scrubber.instance\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">success_callback</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"langfuse\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<p>Set the built-in guardrail to <code>default_on: false</code> while testing. If both it and\nthe callback are masking, you cannot tell which did the work.</p>\n<p>Adding a volume changes the container definition, so this needs a recreate, not\na restart:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"proving-it-on-both-sides-of-one-request\">Proving it, on both sides of one request<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/#proving-it-on-both-sides-of-one-request\" class=\"hash-link\" aria-label=\"Direct link to Proving it, on both sides of one request\" title=\"Direct link to Proving it, on both sides of one request\" translate=\"no\">​</a></h2>\n<p>Send a request and keep the response body:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-small\",\"messages\":[{\"role\":\"user\",\"content\":\"Who is Priya Raghunathan and what is the domain of priya.raghunathan@example.net? One short line.\"}],\"max_tokens\":40}'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> response.json</span><br></div></code></pre></div></div>\n<p>What the caller received:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.choices[0].message.content'</span><span class=\"token plain\"> response.json</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Priya Raghunathan is an AI researcher, and the domain</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">priya.raghunathan@example.net is likely a personal email address.</span><br></div></code></pre></div></div>\n<p>What Langfuse stored:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-u</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$LANGFUSE_PUBLIC_KEY</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string variable\" style=\"color:#36acaa\">$LANGFUSE_SECRET_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token string\" style=\"color:#e3116c\">\"http://your-host:3001/api/public/traces?limit=1\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> trace.json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.data[0].output.content'</span><span class=\"token plain\"> trace.json</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&lt;PERSON&gt; is an AI researcher, and the domain &lt;EMAIL_ADDRESS&gt; is likely a</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">personal email address.</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The same sentence twice — the response with real values, the trace with placeholders\" src=\"https://development-wec.wiline.com/docs/assets/images/gw4-side-by-side-54fd1e85dfe8ba2b6544c1a470fb77f9.png\" width=\"448\" height=\"317\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 5.</strong> Same sentence. One real, one masked.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Langfuse trace with both input and output masked, alongside the unmasked response\" src=\"https://development-wec.wiline.com/docs/assets/images/gw4-scrubber-working-29c3b666ef2aa43daef02208253f2fd4.png\" width=\"1901\" height=\"981\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 6.</strong> Masked on both sides of the trace, untouched for the caller.</p>\n<p>Reading a rendered UI proves less than checking the payloads, so check them:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Priya Raghunathan\"</span><span class=\"token plain\"> response.json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Priya Raghunathan\"</span><span class=\"token plain\"> trace.json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"PERSON\"</span><span class=\"token plain\"> trace.json </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">wc</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-l</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"One short line\"</span><span class=\"token plain\"> trace.json</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">1</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Four grep counts proving the response holds the real name and the trace does not\" src=\"https://development-wec.wiline.com/docs/assets/images/gw4-grep-proof-f38350595a637c78223db0c9f01b0da1.png\" width=\"448\" height=\"177\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 7.</strong> The receipt: <code>1 0 2 1</code>.</p>\n<p>The real name is in what the caller received. It is not in the trace. The\nplaceholder appears twice — input and output. And the last line proves both\nfiles describe the <em>same</em> request, which the first three do not establish on\ntheir own.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can mask what a gateway logs without changing what it returns, and prove it\nby inspecting the raw payloads on both sides of a single request.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-version-that-quietly-costs-you-200-350ms\">The version that quietly costs you 200-350ms<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/#the-version-that-quietly-costs-you-200-350ms\" class=\"hash-link\" aria-label=\"Direct link to The version that quietly costs you 200-350ms\" title=\"Direct link to The version that quietly costs you 200-350ms\" translate=\"no\">​</a></h2>\n<p>The first working version of <code>_mask</code> used a blocking client — <code>httpx.Client</code>\ninside an <code>async def</code>. It masked correctly. It also did this:</p>\n<table><thead><tr><th></th><th>run 1</th><th>run 2</th></tr></thead><tbody><tr><td>no callback</td><td>0.74 s</td><td>—</td></tr><tr><td>blocking <code>httpx.Client</code></td><td>1.10 s</td><td>0.97 s</td></tr><tr><td>async <code>httpx.AsyncClient</code></td><td>0.77 s</td><td>0.77 s</td></tr></tbody></table>\n<p>Warm medians, four sequential requests each, first discarded as cold. The async\nfigure repeated exactly; the blocking one did not, which is itself the point —\nthe penalty depends on what else the loop is doing.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Timed runs with the blocking client and with the async client\" src=\"https://development-wec.wiline.com/docs/assets/images/gw4-timing-97e67eb7eb08fb9770f75d0b038bb147.png\" width=\"386\" height=\"224\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 8.</strong> Same request, same masking, one word different in the code.</p>\n<p>A blocking HTTP call inside an async hook stalls the event loop until Presidio\nanswers, and each request makes several such calls. The masking is not on the\ncaller's critical path — but it is on everyone else's, because nothing else can\nbe serviced while it waits.</p>\n<p>Changing <code>httpx.Client</code> to <code>httpx.AsyncClient</code> and awaiting the calls removes\nthe cost: 0.77s against a 0.74s baseline, inside the noise of the model call\nitself, and it reproduced to the hundredth across two separate runs.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>What we did not test</div><div class=\"admonitionContent_BuS1\"><p>These numbers are four sequential requests. Blocking the event loop barely\nshows at one request at a time — it is under concurrency that the two versions\ndiverge, and we have not measured that here. If you run this at volume, load\ntest it before trusting the table above.</p><p>Each request also means two Presidio HTTP calls per message plus two for the\ncompletion. At real volume Presidio becomes a service you size and monitor,\nnot a background detail.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"where-this-leaves-you\">Where this leaves you<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/#where-this-leaves-you\" class=\"hash-link\" aria-label=\"Direct link to Where this leaves you\" title=\"Direct link to Where this leaves you\" translate=\"no\">​</a></h2>\n<table><thead><tr><th></th><th>Logged input</th><th>Logged output</th><th>Caller's response</th><th>Cost</th></tr></thead><tbody><tr><td><code>filter_scope: input</code></td><td>masked</td><td>raw PII</td><td>untouched</td><td>none</td></tr><tr><td><code>filter_scope: both</code></td><td>masked</td><td>masked</td><td>masked</td><td>0.30s guardrail span</td></tr><tr><td>logging hook</td><td>masked</td><td>masked</td><td>untouched</td><td>none measurable</td></tr></tbody></table>\n<p>Worth being clear about the limit of what this achieves: the PII still travels.\nIt reaches the model and it reaches the caller. What has been eliminated is\n<em>retention</em> — it is no longer sitting in a trace store that a wider group of\npeople can read, months later, long after the request itself is gone.</p>\n<p>Verified against <code>ghcr.io/berriai/litellm:main-stable</code>, admin UI reporting\nv1.96.2, on 21 August 2026. If a later release adds a logging-scoped output\nmode, prefer it over this.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting\">Troubleshooting<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/#troubleshooting\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting\" title=\"Direct link to Troubleshooting\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"traces-never-appear-and-nothing-errors\">Traces never appear and nothing errors<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/#traces-never-appear-and-nothing-errors\" class=\"hash-link\" aria-label=\"Direct link to Traces never appear and nothing errors\" title=\"Direct link to Traces never appear and nothing errors\" translate=\"no\">​</a></h3>\n<p>Check <code>LANGFUSE_HOST</code> is set, not <code>LANGFUSE_BASE_URL</code>, and confirm the variables\nare inside the container with <code>docker exec llm-gateway printenv | grep LANGFUSE</code>.\nA <code>docker compose restart</code> will not have loaded them.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-callback-does-not-run-and-the-gateway-starts-normally\">The callback does not run and the gateway starts normally<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/#the-callback-does-not-run-and-the-gateway-starts-normally\" class=\"hash-link\" aria-label=\"Direct link to The callback does not run and the gateway starts normally\" title=\"Direct link to The callback does not run and the gateway starts normally\" translate=\"no\">​</a></h3>\n<p>A bad dotted path fails quietly. <code>scrubber.py</code> must sit in the same directory as\n<code>config.yaml</code> — inside the container, not just on the host — and the object name\nafter the dot must exist. Grep the startup log for <code>ImportError</code> and\n<code>AttributeError</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-response-comes-back-masked\">The response comes back masked<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/#the-response-comes-back-masked\" class=\"hash-link\" aria-label=\"Direct link to The response comes back masked\" title=\"Direct link to The response comes back masked\" translate=\"no\">​</a></h3>\n<p>The deep copy is missing, or the built-in guardrail is still enabled with an\noutput scope. Set <code>default_on: false</code> on it and confirm with\n<code>curl /guardrails/list</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"everything-got-slower\">Everything got slower<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/#everything-got-slower\" class=\"hash-link\" aria-label=\"Direct link to Everything got slower\" title=\"Direct link to Everything got slower\" translate=\"no\">​</a></h3>\n<p>Check <code>_mask</code> uses <code>httpx.AsyncClient</code> with <code>await</code>, not <code>httpx.Client</code>. See the\nmeasurements above.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Clean traces, untouched answers</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>The measurements here are sequential, and the failure mode that cost 200-350ms\nis one that only bites properly under concurrency. The next post puts the gateway\nunder parallel load and measures what actually happens to latency when several\nrequests contend for the same event loop.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/proxy/guardrails/pii_masking_v2\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">PII, PHI Masking — Presidio</a></li>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/observability/scrub_data\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Scrub Logged Data</a> — the documented hook this post builds on</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/\">Part 3 — mask PII at the gateway</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/",
            "title": "Clean traces, untouched answers: masking PII in LiteLLM's logs without corrupting the response",
            "summary": "Send a gateway's traffic to Langfuse and the model's own reply carries the PII straight back into your traces. Widen Presidio's scope and it masks the answer your users receive instead. Neither setting gives you both, so here is a forty-line callback that does — measured, and with the mistake that quietly makes it slower.",
            "date_modified": "2026-08-21T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "llm",
                "gateway",
                "pii",
                "privacy",
                "guardrails",
                "observability",
                "langfuse",
                "litellm",
                "presidio",
                "wec"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__presidio\" src=\"data:image/webp;base64,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\" alt=\"Presidio\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 5 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->5</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->5<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting an LLM gateway</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">One endpoint, scoped keys</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Route work to the right model</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">3</span><span class=\"skillTracker__skill\" data-state=\"current\">Mask PII at the gateway</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Clean traces, untouched answers</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove it holds under load</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/\">Part 2</a> ended with the gateway deciding\nwhich model answers each request — and still forwarding every prompt verbatim,\nincluding the ones carrying customer names, emails and phone numbers.</p>\n<p>This post puts a PII filter in that path. Not in front of the model, though:\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/news/mask-your-logs-not-your-prompts/\">in front of the logs</a>. The model reading\nyour prompt is doing its job; the risk is what gets <strong>stored</strong>. So the raw text\ngoes to the model and a masked copy goes to your logging.</p>\n<p>That is what the gateway advertises. Following its documented configuration got\nme the opposite — a model that answered correctly and a reply that came back as\n<code>&lt;LOCATION&gt;</code>. This is how to find that and how to fix it.</p>\n<!-- -->\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Version tested</div><div class=\"admonitionContent_BuS1\"><p>LiteLLM <code>1.96.2</code> (<code>ghcr.io/berriai/litellm:main-stable</code>), Presidio images from\n<code>ghcr.io/data-privacy-stack</code>, models <code>qwen-small</code> (Qwen2.5-3B-Instruct) and\n<code>qwen-mid</code> (Qwen3.5-9B) via the WEC Inference API, on 20 August 2026. Defaults\nchange — if your results differ, check your version first.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-presidio-is-and-where-it-goes\">What Presidio is, and where it goes<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#what-presidio-is-and-where-it-goes\" class=\"hash-link\" aria-label=\"Direct link to What Presidio is, and where it goes\" title=\"Direct link to What Presidio is, and where it goes\" translate=\"no\">​</a></h2>\n<p><a href=\"https://github.com/data-privacy-stack/presidio\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Presidio</a> detects personal data\nin text: names, emails, phone numbers, card numbers. You POST it a string and it\nreturns the entities it found, with character offsets and a confidence score.</p>\n<p>It ships as two services — an <strong>analyzer</strong> that finds PII and an <strong>anonymizer</strong>\nthat replaces it. The gateway calls both.</p>\n<p>One naming note that trips people up: Presidio was Microsoft's project and most\ntutorials still call it that. The repository moved to the <code>data-privacy-stack</code>\norganisation, and <code>github.com/microsoft/presidio</code> now answers <code>Moved Permanently</code>. Same project, new home, which is why the images below aren't under\na Microsoft path.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"add-presidio-to-the-gateways-compose\">Add Presidio to the gateway's compose<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#add-presidio-to-the-gateways-compose\" class=\"hash-link\" aria-label=\"Direct link to Add Presidio to the gateway's compose\" title=\"Direct link to Add Presidio to the gateway's compose\" translate=\"no\">​</a></h2>\n<p>Presidio only ever talks to the gateway, so it does not need published ports.\nOpen the <code>docker-compose.yml</code> from Part 1 and add two services after the <code>db:</code>\nblock, above <code>volumes:</code>:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.yml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio-analyzer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> ghcr.io/data</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">privacy</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">stack/presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">analyzer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">latest</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">container_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">analyzer</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">restart</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> unless</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">stopped</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio-anonymizer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> ghcr.io/data</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">privacy</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">stack/presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">anonymizer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">latest</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">container_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">anonymizer</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">restart</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> unless</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">stopped</span><br></div></code></pre></div></div>\n<p>Two spaces of indentation, same level as <code>litellm:</code> and <code>db:</code>.</p>\n<p>Notice what is absent: no <code>ports:</code>. Putting them in the same compose project as\nthe gateway means Docker gives them a shared network and DNS, so the gateway\nreaches them by service name and nothing else on your network can reach them at\nall. If you deployed Presidio separately with <code>ports: [\"5002:3000\"]</code>, that\nexposed your PII analyzer to everything that can route to the host — worth\nundoing.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/llm-gateway </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><br></div></code></pre></div></div>\n<p>Four containers, and the Presidio rows should show <code>3000/tcp</code> with no host\nmapping:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">NAME                  SERVICE               STATUS         PORTS</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">llm-gateway           litellm               Up             127.0.0.1:4000-&gt;4000/tcp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">llm-gateway-db        db                    Up (healthy)   5432/tcp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">presidio-analyzer     presidio-analyzer     Up (healthy)   3000/tcp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">presidio-anonymizer   presidio-anonymizer   Up (healthy)   3000/tcp</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Four containers running, with both Presidio services showing 3000/tcp and no host port mapping\" src=\"https://development-wec.wiline.com/docs/assets/images/gw3-compose-ps-57ff2d738b0cb45457ecc59f8325613d.png\" width=\"1732\" height=\"340\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>If <code>docker compose ps</code> shows only two containers, the compose file was not saved.\nConfirm what compose is actually reading:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose config </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--services</span><br></div></code></pre></div></div>\n<p>That must list four names. It reads the file on disk, so it catches an edit that\nnever made it out of your editor.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"check-the-analyzer-and-learn-its-limits\">Check the analyzer, and learn its limits<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#check-the-analyzer-and-learn-its-limits\" class=\"hash-link\" aria-label=\"Direct link to Check the analyzer, and learn its limits\" title=\"Direct link to Check the analyzer, and learn its limits\" translate=\"no\">​</a></h2>\n<p>Before wiring anything, see what Presidio actually finds:</p>\n<p>There are no host ports now, so ask from inside the gateway. The LiteLLM image\nships no <code>curl</code>, so use its Python:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"import json,urllib.request as u; r=u.Request('http://presidio-analyzer:3000/analyze', data=json.dumps({'text':'Call Rafael on 555-0142 or rafael@example.com','language':'en'}).encode(), headers={'Content-Type':'application/json'}); print(u.urlopen(r).read().decode())\"</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"entity_type\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"EMAIL_ADDRESS\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"score\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1.0</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"start\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">27</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"end\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">45</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"entity_type\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"PERSON\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"score\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0.85</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"start\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">5</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"end\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">11</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"entity_type\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"URL\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"score\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0.5</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"start\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">34</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"end\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">45</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Analyzer response scoring the email at 1.0 and the name at 0.85, with no phone number detected\" src=\"https://development-wec.wiline.com/docs/assets/images/gw3-analyze-scores-16b5f56ac49060ffafbba8dc8a2c2333.png\" width=\"1638\" height=\"488\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Read that carefully, because it sets expectations for everything after.</p>\n<p>It found the email at full confidence and the name at 0.85. <strong>It did not find the\nphone number at all.</strong> <code>555-0142</code> is seven digits with no area code, and\nPresidio validates against real numbering plans rather than pattern-matching\nanything that looks phone-shaped. Add an area code:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"import json,urllib.request as u; r=u.Request('http://presidio-analyzer:3000/analyze', data=json.dumps({'text':'Call Rafael on (415) 555-0142 or rafael@example.com','language':'en'}).encode(), headers={'Content-Type':'application/json'}); print(u.urlopen(r).read().decode())\"</span><br></div></code></pre></div></div>\n<p>Now <code>PHONE_NUMBER</code> appears — scoring <strong>0.4</strong>, against 1.0 for the email.</p>\n<p>It is worth checking whether the <code>555</code> prefix is the problem, since that range is\nreserved for fiction. It is not. A plausible number behaves identically:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> llm-gateway python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"import json,urllib.request as u; r=u.Request('http://presidio-analyzer:3000/analyze', data=json.dumps({'text':'Call Rafael on (415) 682-4531 or on 682-4531','language':'en'}).encode(), headers={'Content-Type':'application/json'}); print(u.urlopen(r).read().decode())\"</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"entity_type\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"PERSON\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"score\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0.85</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"start\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">5</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"end\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">11</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"entity_type\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"PHONE_NUMBER\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"score\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0.4</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"start\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">15</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"end\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">29</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<p>Same 0.4 for the full number, and the bare <code>682-4531</code> is still missed. So two\nthings are true: an area code is what makes a phone number detectable at all,\nand <strong>0.4 is simply what this recognizer returns for phone numbers</strong> — not a\npenalty for fake ones.</p>\n<p>Which means entity types are not equally trustworthy, and confidence is a number\nyou will want to set deliberately. Presidio exposes <code>presidio_score_thresholds</code>\nfor exactly that: a per-entity floor below which detections are discarded. Set it\nat 0.5 and you would silently stop masking every phone number in your traffic.</p>\n<p>Do not build a compliance story on the assumption that this catches everything.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"teaching-it-the-numbers-it-misses\">Teaching it the numbers it misses<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#teaching-it-the-numbers-it-misses\" class=\"hash-link\" aria-label=\"Direct link to Teaching it the numbers it misses\" title=\"Direct link to Teaching it the numbers it misses\" translate=\"no\">​</a></h3>\n<p>There is an escape hatch, and it is worth knowing before you decide the coverage\nis unacceptable. <code>presidio_ad_hoc_recognizers</code> takes a path to a JSON file of\nextra recognizers, loaded when the guardrail starts:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">recognizers.json</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"name\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"LocalPhoneRecognizer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"supported_language\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"en\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"supported_entity\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"PHONE_NUMBER\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"patterns\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"name\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"us-local-7\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"regex\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\\\\b\\\\d{3}-\\\\d{4}\\\\b\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"score\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0.9</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<p>Mount it beside the config and point the guardrail at it:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.yml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./recognizers.json</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/recognizers.json</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div></code></pre></div></div>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio_ad_hoc_recognizers</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> /app/recognizers.json</span><br></div></code></pre></div></div>\n<p>Recreate the container — a new volume needs <code>up -d</code>, not <code>restart</code> — and the same\nprompt that leaked now masks:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Reply OK. Contact &lt;PERSON&gt; on &lt;PHONE_NUMBER&gt; about the invoice.</span><br></div></code></pre></div></div>\n<p><strong>Why the score is 0.9 and not 0.8.</strong> Put <code>682-4531</code> inside a sentence —\n<code>Call me on 682-4531</code> — and the default recognizers return <code>DATE_TIME</code> at\n<strong>0.85</strong>: seven digits with a dash, read as a date. (Pass the number on its own,\nwith no sentence around it, and nothing fires at all; the NER model wants\ncontext before it commits to anything.) At 0.8 your phone recognizer loses the\noverlap and the number is masked as <code>&lt;DATE_TIME&gt;</code>: still redacted, but filed\nunder the wrong entity, which quietly breaks any reporting that counts by type.\nAt 0.86 or above <code>PHONE_NUMBER</code> wins.</p>\n<p>Measured across three runs each, entirely deterministic:</p>\n<table><thead><tr><th>Text</th><th>Default</th><th>Recognizer at 0.8</th><th>Recognizer at 0.9</th></tr></thead><tbody><tr><td><code>Call me on 555-0142</code></td><td><strong>left in the clear</strong></td><td><code>&lt;PHONE_NUMBER&gt;</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td></tr><tr><td><code>Call me on 682-4531</code></td><td><code>&lt;DATE_TIME&gt;</code></td><td><code>&lt;DATE_TIME&gt;</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td></tr><tr><td><code>(415) 682-4531 or on 682-4531</code></td><td>bare one <strong>leaks</strong></td><td>both masked</td><td>both masked</td></tr></tbody></table>\n<p>Note the middle row is not a miss — it is a misclassification, and the anonymizer\nstill redacts it. The first and third rows are the real leaks, and a recognizer\ncloses both.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-version-above-is-not-safe-to-ship\">The version above is not safe to ship<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#the-version-above-is-not-safe-to-ship\" class=\"hash-link\" aria-label=\"Direct link to The version above is not safe to ship\" title=\"Direct link to The version above is not safe to ship\" translate=\"no\">​</a></h3>\n<p>Seven digits and a dash is a shape, not a meaning, and plenty of things share it.\nRun that recognizer against text from an actual support queue:</p>\n<table><thead><tr><th>Text</th><th>Masked as</th></tr></thead><tbody><tr><td><code>Order 482-1099 shipped yesterday</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td></tr><tr><td><code>Invoice 100-2000 is overdue</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td></tr><tr><td><code>Part number 250-4000 is discontinued</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td></tr><tr><td><code>The error code was 500-1001</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td></tr><tr><td><code>Our office is at 200-4500 Main Street</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td></tr><tr><td><code>RFC 793-1981 defines TCP</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td></tr></tbody></table>\n<p>Six for six. And the 0.9 you set to win the <code>DATE_TIME</code> overlap now wins <em>every</em>\noverlap, so the recognizer does not merely add noise — it overwrites correct\nclassifications with a wrong one. Traces full of <code>&lt;PHONE_NUMBER&gt;</code> where the order\nnumbers used to be are worse than traces with one number in the clear, because\nnow you cannot tell which is which.</p>\n<p>The fix is to stop matching on shape alone and require a cue word in front of the\ndigits. That has to be a <strong>lookbehind</strong>: Presidio redacts the whole match, so\n<code>(call\\W+)(\\d{3}-\\d{4})</code> would swallow the word \"call\" along with the number —\ncapture groups do not narrow the span. Python's <code>re</code> only allows fixed-width\nlookbehinds, which is why this is an alternation rather than one tidy list:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">recognizers.json</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"name\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"LocalPhoneRecognizer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"supported_language\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"en\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"supported_entity\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"PHONE_NUMBER\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"patterns\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token property\" style=\"color:#36acaa\">\"name\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"us-local-7-cued\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token property\" style=\"color:#36acaa\">\"regex\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"(?i)(?:(?&lt;=call )|(?&lt;=called )|(?&lt;=calling )|(?&lt;=phone )|(?&lt;=phone: )|(?&lt;=phone is )|(?&lt;=tel )|(?&lt;=tel: )|(?&lt;=cell )|(?&lt;=cell: )|(?&lt;=cell is )|(?&lt;=mobile )|(?&lt;=mobile: )|(?&lt;=contact )|(?&lt;=contact: )|(?&lt;=number )|(?&lt;=number: )|(?&lt;=number is )|(?&lt;=me on )|(?&lt;=him on )|(?&lt;=her on )|(?&lt;=them on )|(?&lt;=us on )|(?&lt;=me at )|(?&lt;=him at )|(?&lt;=her at )|(?&lt;=them at )|(?&lt;=us at ))(?&lt;!part number )(?&lt;!serial number )(?&lt;!order number )(?&lt;!invoice number )(?&lt;!account number )(?&lt;!model number )(?&lt;!tracking number )(?&lt;!reference number )(?&lt;!batch number )(?&lt;!ticket number )(?&lt;!case number )\\\\d{3}-\\\\d{4}\\\\b\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token property\" style=\"color:#36acaa\">\"score\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0.9</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><br></div></code></pre></div></div>\n<p>The trailing negative lookbehinds are there because <code>number </code> is a useful cue and\n<code>part number </code> is not.</p>\n<p>Measured over twelve phone phrasings and twelve lookalikes:</p>\n<table><thead><tr><th></th><th><code>\\b\\d{3}-\\d{4}\\b</code></th><th>Cue-anchored</th></tr></thead><tbody><tr><td>Phone numbers masked</td><td>12/12</td><td>12/12</td></tr><tr><td>Lookalikes wrongly masked</td><td><strong>12/12</strong></td><td><strong>0/12</strong></td></tr></tbody></table>\n<p>Two things this does not fix, and you should know both. <code>Order 482-1099</code> still\ncomes back as <code>&lt;DATE_TIME&gt;</code> — that is spaCy misfiring on its own, with or without\nyour recognizer. And a phone number introduced by a phrase you did not think of\ngoes back to leaking. A cue list is a guess about how people write, so treat it\nas something to revisit against your own traffic rather than a finished artefact.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Brazilian numbers fail the same way</div><div class=\"admonitionContent_BuS1\"><p><code>BR</code> is in the default region list, so <code>(11) 91234-5678</code> is recognised — and\n<code>91234-5678</code> without the DDD is not, exactly as with a missing US area code. The\nsame recognizer closes it with <code>\\d{4,5}-\\d{4}</code> and a Portuguese cue list.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"wire-the-guardrail\">Wire the guardrail<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#wire-the-guardrail\" class=\"hash-link\" aria-label=\"Direct link to Wire the guardrail\" title=\"Direct link to Wire the guardrail\" translate=\"no\">​</a></h2>\n<p>The gateway calls Presidio through a guardrail. Open <code>config.yaml</code> and add a\n<code>guardrails:</code> block at the end — this is the configuration the LiteLLM\ndocumentation gives for masking only on the logging path:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">guardrails</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">guardrail_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">log</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">mask</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_params</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">guardrail</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> presidio</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">mode</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> logging_only</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">default_on</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:#36acaa\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio_analyzer_api_base</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> http</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">//presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">analyzer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">3000</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio_anonymizer_api_base</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> http</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">//presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">anonymizer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">3000</span><br></div></code></pre></div></div>\n<p><code>mode: logging_only</code> is the important one. The documentation describes it as:</p>\n<blockquote>\n<p>Run after LLM call, only apply PII Masking before logging to Langfuse, etc.\nNot on the actual llm api request / response.</p>\n</blockquote>\n<p><code>default_on: true</code> applies it to every request rather than only those that ask\nfor it by name. And the two <code>api_base</code> values are the service names from the\ncompose file — the guardrail runs inside the gateway container, so <code>localhost</code>\nwould point at the wrong place.</p>\n<p>Restart, and wait for the proxy to actually come back — it takes 30 to 60\nseconds, which is longer than most <code>sleep</code> commands people put in front of it:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose restart litellm</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">until</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sf</span><span class=\"token plain\"> localhost:4000/health/readiness </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">printf</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.'</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\" ready\"</span><br></div></code></pre></div></div>\n<p>Then confirm the guardrail loaded, which is more reliable than reading logs:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">grep</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-m1</span><span class=\"token variable\" style=\"color:#36acaa\"> LITELLM_MASTER_KEY ~/llm-gateway/.env </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">cut</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\"> -f2- </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">tr</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'\"'</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> localhost:4000/guardrails/list </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"import sys,json; g=json.load(sys.stdin)['guardrails'][0]; p=g['litellm_params']; print(json.dumps({'guardrail_name': g['guardrail_name'], **{k: p.get(k) for k in ('guardrail','mode','presidio_filter_scope','default_on','presidio_analyzer_api_base','presidio_anonymizer_api_base','fail_on_error','unreachable_fallback')}}, indent=2))\"</span><br></div></code></pre></div></div>\n<p>The endpoint returns every possible guardrail field, most of them <code>null</code>, so this\npicks out the ones that matter:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"guardrail_name\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"presidio-log-mask\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"guardrail\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"presidio\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"mode\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"logging_only\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"presidio_filter_scope\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"input\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"default_on\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">true</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"presidio_analyzer_api_base\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://presidio-analyzer:3000\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"presidio_anonymizer_api_base\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://presidio-anonymizer:3000\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"fail_on_error\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">true</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"unreachable_fallback\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"fail_closed\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>Those last two are the defaults worth knowing before this sees real traffic: if\nPresidio is unreachable, requests fail rather than passing unmasked.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The guardrails list endpoint reporting presidio-log-mask loaded, with mode logging_only and both Presidio API bases resolved\" src=\"https://development-wec.wiline.com/docs/assets/images/gw3-guardrail-list-cd2a45b09b50cc051970b5b85feba496.png\" width=\"1632\" height=\"732\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"test-it-and-get-a-surprise\">Test it, and get a surprise<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#test-it-and-get-a-surprise\" class=\"hash-link\" aria-label=\"Direct link to Test it, and get a surprise\" title=\"Direct link to Test it, and get a surprise\" translate=\"no\">​</a></h2>\n<p>Now the part that matters. Send a prompt whose <em>answer</em> proves whether the model\nsaw the real text — asking it to echo the PII back is useless, because the reply\ngets scanned too:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">grep</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-m1</span><span class=\"token variable\" style=\"color:#36acaa\"> LITELLM_MASTER_KEY ~/llm-gateway/.env </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">cut</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\"> -f2- </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">tr</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'\"'</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> localhost:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-small\",\"temperature\":0,\"messages\":[{\"role\":\"user\",\"content\":\"Which US city does this area code belong to: (415) 555-0142? Answer with only the city name.\"}]}'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"import sys,json; print(json.load(sys.stdin)['choices'][0]['message']['content'])\"</span><br></div></code></pre></div></div>\n<p>Expected: <code>San Francisco</code>, proving the model read the digits.</p>\n<p>Actual:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">&lt;LOCATION&gt;</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The model answering a question that requires reading the area code, and the answer returned as the placeholder LOCATION\" src=\"https://development-wec.wiline.com/docs/assets/images/gw3-masked-answer-1467b181d83013e63ad42b11ffcdfa95.png\" width=\"1416\" height=\"248\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>The model answered a question that requires reading a real area code — and then\nthe answer itself came back masked. <code>logging_only</code> was supposed to stay off the\nactual response.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"why-that-happens\">Why that happens<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#why-that-happens\" class=\"hash-link\" aria-label=\"Direct link to Why that happens\" title=\"Direct link to Why that happens\" translate=\"no\">​</a></h2>\n<p>There is a second setting, and its default is doing the work.</p>\n<p><code>presidio_filter_scope</code> controls which direction gets scanned. From the\ndocumentation:</p>\n<blockquote>\n<p><code>input</code>: only user → model content is scanned; <code>output</code>: only model → user\ncontent is scanned; <code>both</code> (default): scan both directions</p>\n</blockquote>\n<p>And, on the same page:</p>\n<blockquote>\n<p>Use <code>presidio_filter_scope: output</code> (or <code>both</code>) when you want Presidio to\nactively scan and mask the model's response before it reaches the user.</p>\n</blockquote>\n<p>So <code>both</code> — the default — actively masks responses. The documented <code>logging_only</code>\nexample does not set <code>presidio_filter_scope</code> at all, which leaves it at <code>both</code>.\nThe result is a configuration that masks the thing the same page says it will not\ntouch.</p>\n<p>In the source, <code>initialize_presidio</code> reads that scope and registers a second\ncallback with <code>apply_to_output=True</code> on <code>post_call</code> whenever output scanning is\nenabled. That callback's event hook is hard-coded, so the <code>mode</code> you set never\nreaches it.</p>\n<p>This has been reported.\n<a href=\"https://github.com/BerriAI/litellm/issues/30447\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Issue #30447</a>, <em>\"logging_only\nPresidio guardrail corrupts user-facing response (assistant content replaced with\nPII tokens)\"</em>, was filed on 15 June 2026 and closed the next day with no\ndiscussion, alongside a pull request titled <em>\"fix(presidio): don't mask the live\nrequest when guardrail is logging_only\"</em>. The report was about the response; the\nfix addressed the request. On the <code>main-stable</code> image used here, the behaviour\nabove still reproduces.</p>\n<p>A second report is open:\n<a href=\"https://github.com/BerriAI/litellm/issues/35951\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">#35951</a> notes that in\n<code>logging_only</code> mode the masking hook rewrites <code>kwargs[\"messages\"]</code> but does not\npropagate to <code>standard_logging_object</code>, which is what downstream loggers read.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-fix\">The fix<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#the-fix\" class=\"hash-link\" aria-label=\"Direct link to The fix\" title=\"Direct link to The fix\" translate=\"no\">​</a></h2>\n<p>One line. Add <code>presidio_filter_scope: input</code> to the guardrail:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">guardrails</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">guardrail_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">log</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">mask</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_params</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">guardrail</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> presidio</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">mode</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> logging_only</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio_filter_scope</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> input</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">default_on</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:#36acaa\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio_analyzer_api_base</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> http</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">//presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">analyzer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">3000</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">presidio_anonymizer_api_base</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> http</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">//presidio</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">anonymizer</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">3000</span><br></div></code></pre></div></div>\n<p>Restart, wait for readiness, and run the same request:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">San Francisco</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can run Presidio beside a gateway, mask prompts at logging time without\ntouching the live request, and tell the two directions apart with\n<code>presidio_filter_scope</code>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"proving-it-properly\">Proving it properly<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#proving-it-properly\" class=\"hash-link\" aria-label=\"Direct link to Proving it properly\" title=\"Direct link to Proving it properly\" translate=\"no\">​</a></h2>\n<p>One right answer could be a lucky guess — \"San Francisco\" is a plausible default\nfor a masked area-code question, and <code>temperature: 0</code> makes a guess repeat just\nas reliably as a real answer. So ask about several area codes the model cannot\nbluff:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">grep</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-m1</span><span class=\"token variable\" style=\"color:#36acaa\"> LITELLM_MASTER_KEY ~/llm-gateway/.env </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">cut</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\"> -f2- </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">tr</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'\"'</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">ac</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">907</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">808</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">216</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">505</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">printf</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"area %s -&gt; \"</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$ac</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> localhost:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"{</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">model</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">qwen-mid</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">,</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">temperature</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:0,</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">messages</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:[{</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">role</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">user</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">,</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">content</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">Which US state does this phone number's area code belong to: (</span><span class=\"token string variable\" style=\"color:#36acaa\">$ac</span><span class=\"token string\" style=\"color:#e3116c\">) 555-0142? Answer with only the state name.</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">}]}\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"import sys,json; print(' '.join(json.load(sys.stdin)['choices'][0]['message']['content'].split()))\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">done</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">area 907 -&gt; Alaska</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">area 808 -&gt; Hawaii</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">area 216 -&gt; Ohio</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">area 505 -&gt; New Mexico</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Four different area codes each returning the correct state, proving the prompt reached the model unmasked\" src=\"https://development-wec.wiline.com/docs/assets/images/gw3-area-codes-9448b7ab1d15d52e9cafb0d8ad0e5b86.png\" width=\"1574\" height=\"356\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Four for four. The model is reading the digits, so the prompt is reaching it\nunmasked. That is the behaviour this post set out to get.</p>\n<p>The whitespace collapse in that command is not cosmetic fussiness — <code>qwen-mid</code> is\na reasoning model and prefixes its answers with newlines, inconsistently enough\nthat <code>.strip()</code> alone leaves ragged output.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"why-not-just-mask-before-the-model\">Why not just mask before the model?<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#why-not-just-mask-before-the-model\" class=\"hash-link\" aria-label=\"Direct link to Why not just mask before the model?\" title=\"Direct link to Why not just mask before the model?\" translate=\"no\">​</a></h2>\n<p>It is the obvious question, and the obvious answer is wrong. <code>mode: pre_call</code>\nmasks the prompt before the model ever sees it — which sounds like the safest\npossible arrangement.</p>\n<p>Change the one line and find out:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">mode</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> pre_call</span><br></div></code></pre></div></div>\n<p>Restart, then ask the same question:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">grep</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-m1</span><span class=\"token variable\" style=\"color:#36acaa\"> LITELLM_MASTER_KEY ~/llm-gateway/.env </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">cut</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\"> -f2- </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">tr</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'\"'</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> localhost:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'Content-Type: application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-small\",\"temperature\":0,\"max_tokens\":60,\"messages\":[{\"role\":\"user\",\"content\":\"Which US state does this phone number'</span><span class=\"token plain\">\"</span><span class=\"token string\" style=\"color:#e3116c\">'\"'</span><span class=\"token plain\">s area code belong to: </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token number\" style=\"color:#36acaa\">907</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">555</span><span class=\"token plain\">-0142? Answer with only the state name.</span><span class=\"token string\" style=\"color:#e3116c\">\"}]}' \\</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  | python3 -c \"</span><span class=\"token function\" style=\"color:#d73a49\">import</span><span class=\"token plain\"> sys,json</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">' '</span><span class=\"token plain\">.join</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">json.load</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sys.stdin</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">'choices'</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">'message'</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">'content'</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\">.split</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">))</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">To determine the state for a given phone number's area code, I would need to know</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">the specific area code of the phone number provided. Please provide the area code</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">or the full phone number so I can identify the corresponding state.</span><br></div></code></pre></div></div>\n<p>The exact wording moves around between runs even at <code>temperature: 0</code> — the point\nis that every version of it asks you for the number you already sent.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Under pre_call masking the model asks for the phone number it was already given, having received only a placeholder\" src=\"https://development-wec.wiline.com/docs/assets/images/gw3-precall-blind-12b8776adeda133b79cac28a0bb36166.png\" width=\"1564\" height=\"308\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>The model is politely asking for the number you just gave it. It received\n<code>&lt;PHONE_NUMBER&gt;</code> and there is nothing in a placeholder to reason about. Nothing\nfailed, nothing errored — you simply got a useless answer, which is the failure\nmode that is hardest to notice in production.</p>\n<p><code>qwen-mid</code> behaved worse in my runs: the same request timed out twice at 40\nseconds with <code>max_tokens: 60</code>, where it answered in well under a second on\nunmasked input. I would not claim a mechanism from two samples, but if you are\nrouting to a reasoning model, time this before trusting it.</p>\n<p>So the three placements, same question each time:</p>\n<table><thead><tr><th>Configuration</th><th>Model receives</th><th>Result</th></tr></thead><tbody><tr><td><code>mode: pre_call</code></td><td><code>&lt;PHONE_NUMBER&gt;</code></td><td>asks you for the number it was given</td></tr><tr><td><code>mode: logging_only</code>, scope left default</td><td>real digits</td><td>correct answer, returned as <code>&lt;LOCATION&gt;</code></td></tr><tr><td><code>mode: logging_only</code>, <code>presidio_filter_scope: input</code></td><td>real digits</td><td><code>San Francisco</code></td></tr></tbody></table>\n<p>Only the third gives you a working application. Put <code>mode</code> back to\n<code>logging_only</code> before continuing.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-this-gives-you-and-what-it-does-not\">What this gives you, and what it does not<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#what-this-gives-you-and-what-it-does-not\" class=\"hash-link\" aria-label=\"Direct link to What this gives you, and what it does not\" title=\"Direct link to What this gives you, and what it does not\" translate=\"no\">​</a></h2>\n<p>You now have prompts reaching models intact, with PII masking attached to the\nlogging path instead of the request path — and one setting away from silently\nredacting your users' answers.</p>\n<p>One thing this fix does not cover, and it matters: <code>input</code> scope masks the\nprompt only. Whatever the model <em>says</em> is logged verbatim — and a model asked\nabout a customer will repeat that customer's name back. So the PII you removed\nfrom the prompt reappears in the completion. Widening the scope to <code>both</code> closes\nthat hole and reintroduces the response corruption above; there is no setting\nthat does both.\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/\">Part 4</a> measures the leak\nand builds the missing piece on <code>async_logging_hook</code>, the documented callback\nthat runs after the response has been sent.</p>\n<p>Being straight about the boundary: the masked copy lands wherever your logging\ncallbacks send it. The gateway's own <code>LiteLLM_SpendLogs</code> table is not one of\nthose — <code>messages</code> stayed <code>{}</code> in every configuration I tried, including with\n<code>store_prompts_in_spend_logs</code> set both in <code>config.yaml</code> and as an environment\nvariable. The documentation points at Langfuse and similar destinations, which is\nwhere the next post looks. <strong>Until you have wired one up and seen a masked\npayload in it, treat the masking half as unproven in your own deployment.</strong></p>\n<p>Two more things worth knowing before this sits in front of real traffic. The\nguardrail defaults to <code>fail_on_error: true</code> with <code>unreachable_fallback: fail_closed</code>, so if Presidio goes down your requests fail rather than passing\nunmasked — the right default, but plan for it. And every Presidio call adds\nlatency to the logging path; Part 2's timing method applies here too.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting\">Troubleshooting<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#troubleshooting\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting\" title=\"Direct link to Troubleshooting\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"docker-compose-ps-shows-two-containers-not-four\"><code>docker compose ps</code> shows two containers, not four<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#docker-compose-ps-shows-two-containers-not-four\" class=\"hash-link\" aria-label=\"Direct link to docker-compose-ps-shows-two-containers-not-four\" title=\"Direct link to docker-compose-ps-shows-two-containers-not-four\" translate=\"no\">​</a></h3>\n<p>The compose file was not saved. <code>docker compose config --services</code> reads what is\non disk — if Presidio is missing there, the edit did not land.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"curl-executable-file-not-found-in-path\"><code>curl: executable file not found in $PATH</code><a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#curl-executable-file-not-found-in-path\" class=\"hash-link\" aria-label=\"Direct link to curl-executable-file-not-found-in-path\" title=\"Direct link to curl-executable-file-not-found-in-path\" translate=\"no\">​</a></h3>\n<p>The LiteLLM image has no <code>curl</code>. Probe with the <code>python -c</code> one-liner above.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"name-or-service-not-known-reaching-presidio-analyzer\"><code>Name or service not known</code> reaching <code>presidio-analyzer</code><a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#name-or-service-not-known-reaching-presidio-analyzer\" class=\"hash-link\" aria-label=\"Direct link to name-or-service-not-known-reaching-presidio-analyzer\" title=\"Direct link to name-or-service-not-known-reaching-presidio-analyzer\" translate=\"no\">​</a></h3>\n<p>The two stacks are on different Docker networks. Both sets of services must be in\nthe same compose project, or share an external network.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"startup-fails-with-validationerror-mode-field-required\">Startup fails with <code>ValidationError: mode Field required</code><a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#startup-fails-with-validationerror-mode-field-required\" class=\"hash-link\" aria-label=\"Direct link to startup-fails-with-validationerror-mode-field-required\" title=\"Direct link to startup-fails-with-validationerror-mode-field-required\" translate=\"no\">​</a></h3>\n<p><code>mode</code> is mandatory on a guardrail. Omitting it exits the proxy on startup —\n<code>Application startup failed. Exiting.</code> You cannot express logging-only by leaving\n<code>mode</code> out.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-model-answers-correctly-but-the-reply-is-location-or-phone_number\">The model answers correctly but the reply is <code>&lt;LOCATION&gt;</code> or <code>&lt;PHONE_NUMBER&gt;</code><a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#the-model-answers-correctly-but-the-reply-is-location-or-phone_number\" class=\"hash-link\" aria-label=\"Direct link to the-model-answers-correctly-but-the-reply-is-location-or-phone_number\" title=\"Direct link to the-model-answers-correctly-but-the-reply-is-location-or-phone_number\" translate=\"no\">​</a></h3>\n<p><code>presidio_filter_scope</code> is at its <code>both</code> default. Set it to <code>input</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"a-phone-number-is-not-being-masked\">A phone number is not being masked<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#a-phone-number-is-not-being-masked\" class=\"hash-link\" aria-label=\"Direct link to A phone number is not being masked\" title=\"Direct link to A phone number is not being masked\" translate=\"no\">​</a></h3>\n<p>Check the score. <code>555-0142</code> is not detected at all; <code>(415) 555-0142</code> scores 0.4.\nUse <code>presidio_score_thresholds</code> to set a per-entity floor.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"changes-to-configyaml-seem-to-have-no-effect\">Changes to <code>config.yaml</code> seem to have no effect<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#changes-to-configyaml-seem-to-have-no-effect\" class=\"hash-link\" aria-label=\"Direct link to changes-to-configyaml-seem-to-have-no-effect\" title=\"Direct link to changes-to-configyaml-seem-to-have-no-effect\" translate=\"no\">​</a></h3>\n<p>Edit the file, <em>then</em> restart — in that order. And a <code>cd</code> from an earlier step\ncarries over, so check which directory you are in before running <code>docker compose</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"env-changes-are-ignored\"><code>.env</code> changes are ignored<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#env-changes-are-ignored\" class=\"hash-link\" aria-label=\"Direct link to env-changes-are-ignored\" title=\"Direct link to env-changes-are-ignored\" translate=\"no\">​</a></h3>\n<p><code>docker compose restart</code> does not reload environment. Use\n<code>docker compose up -d --force-recreate litellm</code>.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Mask PII at the gateway</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>Masking is configured, but you have not yet watched a masked payload land\nanywhere. The next post wires a real logging destination to the gateway and looks\nfor <code>&lt;PERSON&gt;</code> in a trace — closing the loop this one deliberately leaves open —\nand then turns to budgets and what the whole arrangement actually costs.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/proxy/guardrails/pii_masking_v2\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">PII, PHI Masking — Presidio</a> — the guardrail reference, including <code>presidio_filter_scope</code> and <code>presidio_score_thresholds</code></li>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/proxy/logging\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Logging, Alerting, Metrics</a> — the destinations a masked copy can go to</li>\n<li class=\"\"><a href=\"https://github.com/data-privacy-stack/presidio\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Presidio</a> — the analyzer and anonymizer</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/news/mask-your-logs-not-your-prompts/\">Mask your logs, not your prompts</a> — why masking belongs on the storage path</li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\">Part 1 — deploy an LLM gateway on a WEC Instance</a></li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/\">Part 2 — complexity routing and what it costs</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/",
            "title": "Mask PII at the gateway: set up Presidio, plus the one line the docs leave out",
            "summary": "Set up PII masking on a self-hosted LLM gateway with Presidio. Masking can sit in three places — before the model, on the response, or only on the path to your logs — and only one keeps your app working. Follow the documented config and your model's answers come back redacted; here is why, and the single setting that fixes it.",
            "date_modified": "2026-08-20T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "llm",
                "gateway",
                "pii",
                "privacy",
                "guardrails",
                "self-hosting",
                "litellm",
                "presidio",
                "wec"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/qwen-209001e8f781e3651b8d7b863343f2dc.png\" alt=\"Qwen\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 5 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->5</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->5<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting an LLM gateway</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">One endpoint, scoped keys</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">2</span><span class=\"skillTracker__skill\" data-state=\"current\">Route work to the right model</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Mask PII at the gateway</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Clean traces, untouched answers</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove it holds under load</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\">Part 1</a> ended on an uncomfortable\nnumber. The same three-word answer cost 3 tokens from a small model and 200 from\na reasoning model — which spent all 200 thinking and returned nothing at all.</p>\n<p>Every request your apps send picks a model, and mostly that choice is made once,\nhardcoded, and never revisited. This post puts the gateway in charge of it\ninstead: classify the request, route it to a model sized for the work. Then it\nmeasures what that decision costs, because it is not free and most write-ups\nskip that part.</p>\n<!-- -->\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"adding-litellms-complexity-router\">Adding LiteLLM's complexity router<a href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/#adding-litellms-complexity-router\" class=\"hash-link\" aria-label=\"Direct link to Adding LiteLLM's complexity router\" title=\"Direct link to Adding LiteLLM's complexity router\" translate=\"no\">​</a></h2>\n<p>The gateway from Part 1 already has three models registered. A router is just\nanother entry in <code>model_list</code> that maps complexity tiers onto them:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">model_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> smart</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">router</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_params</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">model</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> auto_router/complexity_router</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">complexity_router_config</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">tiers</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">SIMPLE</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> qwen</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">small</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">MEDIUM</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> qwen</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">mid</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">COMPLEX</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> qwen</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">large</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">REASONING</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> qwen</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">large</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">return_raw_model_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:#36acaa\">true</span><br></div></code></pre></div></div>\n<p><code>return_raw_model_name: true</code> is the important one for now. Without it the\nresponse reports <code>smart-router</code> and you have no idea which model served you.\nWith it, the response names the model that actually ran — so every test below\nis self-verifying.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose restart litellm</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"watching-it-route\">Watching it route<a href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/#watching-it-route\" class=\"hash-link\" aria-label=\"Direct link to Watching it route\" title=\"Direct link to Watching it route\" translate=\"no\">​</a></h2>\n<p>You'll send the same request many times with only the prompt changing, so wrap\nit once:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function-name function\" style=\"color:#d73a49\">ask</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"{</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">model</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">smart-router</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">,</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">messages</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:[{</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">role</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">user</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">,</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">content</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$1</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">}],</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">max_tokens</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:10}\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> .model</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>Then five prompts of increasing difficulty:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ask </span><span class=\"token string\" style=\"color:#e3116c\">\"hi\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ask </span><span class=\"token string\" style=\"color:#e3116c\">\"what is a vpc\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ask </span><span class=\"token string\" style=\"color:#e3116c\">\"write a python function that retries an http call with backoff\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ask </span><span class=\"token string\" style=\"color:#e3116c\">\"our two services deadlock under load, walk me through diagnosing it\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ask </span><span class=\"token string\" style=\"color:#e3116c\">\"prove that the halting problem is undecidable\"</span><br></div></code></pre></div></div>\n<p>One line per prompt, in the order sent:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen3.5-9B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen2.5-3B-Instruct</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Five prompts routed by the heuristic scorer, four landing on the 3B model\" src=\"https://development-wec.wiline.com/docs/assets/images/gw2-heuristic-route-af0d1b1a3a318287720fa1b18160e760.png\" width=\"1806\" height=\"450\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> The code request went up a tier. The two hardest questions did not.</p>\n<p>A greeting on the small model is right. A Python request on the mid model is\nright. But a distributed-systems deadlock and one of the hardest questions in\ncomputer science both landed on a 3-billion-parameter model.</p>\n<p>To understand why, you have to know what the router is actually doing.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-a-heuristic-is-and-how-litellm-scores-one\">What a heuristic is, and how LiteLLM scores one<a href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/#what-a-heuristic-is-and-how-litellm-scores-one\" class=\"hash-link\" aria-label=\"Direct link to What a heuristic is, and how LiteLLM scores one\" title=\"Direct link to What a heuristic is, and how LiteLLM scores one\" translate=\"no\">​</a></h2>\n<p>By default this router makes zero API calls. It scores the prompt locally with\npattern matching — that's what \"heuristic\" means here: a cheap rule of thumb\nthat approximates a judgement without making it.</p>\n<p>It scores seven dimensions, each producing a value between −1 and +1, then\nmultiplies each by a fixed weight and adds them up:</p>\n<table><thead><tr><th>Dimension</th><th>Weight</th><th>Fires on</th></tr></thead><tbody><tr><td><code>codePresence</code></td><td>0.30</td><td><code>function</code>, <code>class</code>, <code>api</code>, <code>schema</code>, …</td></tr><tr><td><code>reasoningMarkers</code></td><td>0.25</td><td>\"step by step\", \"think through\", \"analyze\"</td></tr><tr><td><code>technicalTerms</code></td><td>0.25</td><td>\"architecture\", \"distributed\", \"encryption\"</td></tr><tr><td><code>tokenCount</code></td><td>0.10</td><td><strong>−1.0</strong> under 15 tokens, <strong>+1.0</strong> over 400</td></tr><tr><td><code>simpleIndicators</code></td><td>0.05</td><td>\"what is\", \"define\", greetings — scores <strong>−1.0</strong></td></tr><tr><td><code>multiStepPatterns</code></td><td>0.03</td><td>\"first… then\", numbered steps</td></tr><tr><td><code>questionComplexity</code></td><td>0.02</td><td>more than three question marks</td></tr></tbody></table>\n<p>The weighted sum maps to a tier at three boundaries: below <strong>0.15</strong> is SIMPLE,\nbelow <strong>0.35</strong> MEDIUM, below <strong>0.60</strong> COMPLEX, and above that REASONING. Those\nvalues are all documented and all configurable.</p>\n<p>Two dimensions can push the score <em>down</em>. A short prompt scores −1.0 on\n<code>tokenCount</code>. A prompt containing \"what is\" scores −1.0 on <code>simpleIndicators</code>.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-arithmetic-on-a-real-prompt\">The arithmetic, on a real prompt<a href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/#the-arithmetic-on-a-real-prompt\" class=\"hash-link\" aria-label=\"Direct link to The arithmetic, on a real prompt\" title=\"Direct link to The arithmetic, on a real prompt\" translate=\"no\">​</a></h2>\n<p>The router records its own working. Every routed request writes a decision into\nthe spend log:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/spend/logs </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.[].metadata.routing_decision | select(.) | \"\\(.tier)  \\(.score)  -&gt; \\(.routed_model)  \\(.signals)\"'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">head</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-5</span><br></div></code></pre></div></div>\n<p>Newest first, so this reads bottom-up against the order the prompts were sent:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">SIMPLE  -0.1  -&gt; qwen-small  [\"short (11 tokens)\"]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">SIMPLE  0  -&gt; qwen-small  null</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MEDIUM  0.3  -&gt; qwen-mid  [\"code (function, python)\"]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">SIMPLE  -0.15000000000000002  -&gt; qwen-small  [\"short (3 tokens)\",\"simple (what is)\"]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">SIMPLE  -0.15000000000000002  -&gt; qwen-small  [\"short (0 tokens)\",\"simple (hi)\"]</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Routing decisions showing tier, score and the signals behind each\" src=\"https://development-wec.wiline.com/docs/assets/images/gw2-decisions-c9f72192a937dfe8928c7a438f555144.png\" width=\"1788\" height=\"274\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> Not a black box — the router reports the score and the signals it fired.</p>\n<p>Take the last one. <code>prove that the halting problem is undecidable</code>:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">tokenCount        -1.0 × 0.10 = -0.10    11 tokens, under the 15 threshold</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">codePresence       0.0 × 0.30 =  0.00    no code keywords</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">reasoningMarkers   0.0 × 0.25 =  0.00    \"prove\" is not in the marker list</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">technicalTerms     0.0 × 0.25 =  0.00</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">simpleIndicators   0.0 × 0.05 =  0.00</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">multiStepPatterns  0.0 × 0.03 =  0.00</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">questionComplexity 0.0 × 0.02 =  0.00</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                ------</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                 -0.10   below 0.15 → SIMPLE → 3B model</span><br></div></code></pre></div></div>\n<p>Every term is zero except a penalty for being short. The question gets routed\ndown <em>because it is brief</em>.</p>\n<p>Now the deadlock prompt, which is worse: it scored <strong>0.00 with no signals at\nall</strong>. Not a low score — nothing matched. \"Our two services deadlock under load,\nwalk me through diagnosing it\" contains no keyword the scorer recognises, so it\nfalls to SIMPLE by default.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can read a routing decision — tier, score, and the signals that produced it\n— and reproduce the arithmetic by hand from the dimension weights.</p></div></div>\n<p>The router isn't malfunctioning. It is doing exactly what its rules say. The\nrules just have no way to see difficulty that isn't spelled out in vocabulary\nit knows.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Where this bites</div><div class=\"admonitionContent_BuS1\"><p>The failure mode is systematic, not random: <strong>short prompts that need deep\nthinking get routed down</strong>. Those are also the prompts where a wrong model is\nmost obvious to the user.</p></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"a-keyword-never-matches-its-own-plural\">A keyword never matches its own plural<a href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/#a-keyword-never-matches-its-own-plural\" class=\"hash-link\" aria-label=\"Direct link to A keyword never matches its own plural\" title=\"Direct link to A keyword never matches its own plural\" translate=\"no\">​</a></h3>\n<p>Single-word keywords are matched on word boundaries, so <code>endpoint</code> does not match\n<code>endpoints</code>. Every single-word keyword in the default lists is singular, and none\nof them match a plural.</p>\n<p>Whether that changes anything depends on how close the score already sits to a\nboundary, because the dimensions are stepped rather than linear — <code>technicalTerms</code>\nscores 0.5 at two matches and 1.0 at four, so losing one match often changes\nnothing. Sometimes it changes the model:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Review our api endpoint  for authentication and authorization problems → COMPLEX +0.425</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Review our api endpoints for authentication and authorization problems → MEDIUM  +0.275</span><br></div></code></pre></div></div>\n<p>One letter, one tier. The direction is always the same: a plural scores lower or\nequal, never higher, so the drift is toward the cheaper model.</p>\n<p>Plurals you care about can be appended to the technical list with\n<code>custom_technical_keywords</code>. There is no equivalent for code keywords — the only\nlever is <code>code_keywords</code>, which replaces the built-in list rather than extending\nit.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"asking-a-model-instead\">Asking a model instead<a href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/#asking-a-model-instead\" class=\"hash-link\" aria-label=\"Direct link to Asking a model instead\" title=\"Direct link to Asking a model instead\" translate=\"no\">​</a></h2>\n<p>The alternative is to spend a model call on the decision. Four lines:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">classifier_type</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> llm</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">classifier_llm_config</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">model</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> qwen</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">small</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">timeout_ms</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3000</span><br></div></code></pre></div></div>\n<p>Now the router sends the prompt to <code>qwen-small</code> with a rubric and a schema that\nforces back exactly one of <code>SIMPLE</code>, <code>MEDIUM</code>, <code>COMPLEX</code>, <code>REASONING</code>, and routes\non the answer. The classifier here is the <em>cheapest</em> model we have — the same 3B\nthat was wrongly answering the hard questions a moment ago. It turns out to be a\nbetter judge of difficulty than it is an answerer of it.</p>\n<p>Restart, then re-run the identical five prompts:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen3.5-9B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen3.5-122B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen3.5-122B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen3.5-122B</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The same five prompts under the LLM classifier, with the hard ones now on the 122B model\" src=\"https://development-wec.wiline.com/docs/assets/images/gw2-llm-route-9a7f2aa9f50bca506a1786ae36cf8c3b.png\" width=\"1262\" height=\"370\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> Both failures corrected — and two prompts moved up that arguably shouldn't have.</p>\n<table><thead><tr><th>Prompt</th><th>Heuristic</th><th>LLM classifier</th></tr></thead><tbody><tr><td><code>hi</code></td><td>3B</td><td>3B</td></tr><tr><td><code>what is a vpc</code></td><td>3B</td><td>9B</td></tr><tr><td><code>python function … backoff</code></td><td>9B</td><td>122B</td></tr><tr><td><code>deadlock under load</code></td><td>3B ✗</td><td>122B ✓</td></tr><tr><td><code>halting problem</code></td><td>3B ✗</td><td>122B ✓</td></tr></tbody></table>\n<p>The two broken cases are fixed. But read rows 2 and 3 again — everything moved\nup. \"What is a vpc\" is a factual lookup a 3B model answers perfectly well, and\nit now runs on the 9B. You stopped under-serving hard prompts and started\nover-serving easy ones. Whether that trade is worth it depends on your traffic\nmix, and you should measure yours rather than trust this table.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-it-costs\">What it costs<a href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/#what-it-costs\" class=\"hash-link\" aria-label=\"Direct link to What it costs\" title=\"Direct link to What it costs\" translate=\"no\">​</a></h2>\n<p>This is the part that gets left out. Add a twin that skips the router, then time\nboth:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function-name function\" style=\"color:#d73a49\">direct</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"{</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">model</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">qwen-small</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">,</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">messages</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:[{</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">role</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">user</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">,</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">content</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$1</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">}],</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">max_tokens</span><span class=\"token string entity\" style=\"color:#36acaa\">\\\"</span><span class=\"token string\" style=\"color:#e3116c\">:10}\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> .model</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">time</span><span class=\"token plain\"> ask </span><span class=\"token string\" style=\"color:#e3116c\">\"hi\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">time</span><span class=\"token plain\"> ask </span><span class=\"token string\" style=\"color:#e3116c\">\"hi\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">time</span><span class=\"token plain\"> direct </span><span class=\"token string\" style=\"color:#e3116c\">\"hi\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen2.5-3B-Instruct     real  0m2.474s     ← first call after restart</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen2.5-3B-Instruct     real  0m1.041s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">qwen-small              real  0m0.529s</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"time output comparing a routed call against a direct call\" src=\"https://development-wec.wiline.com/docs/assets/images/gw2-timing-ea97fdf0902b60a49ea16f3951a173c6.png\" width=\"1802\" height=\"722\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 4.</strong> Same prompt, same model answers it, twice the wall clock.</p>\n<p>The same 3B model produced both answers. The only difference is that one request\nwas classified first. Now decompose it — the spend log times each call\nseparately:</p>\n<table><thead><tr><th>Wall clock</th><th>Classifier call</th><th>Serving call</th></tr></thead><tbody><tr><td>2.474 s (cold)</td><td>1449 ms</td><td>806 ms</td></tr><tr><td>1.041 s</td><td>549 ms</td><td>456 ms</td></tr><tr><td>0.529 s (direct)</td><td>—</td><td>495 ms</td></tr></tbody></table>\n<p>The arithmetic closes: 549 + 456 = 1005 ms against 1.041 s measured at the shell.</p>\n<p>Three things fall out:</p>\n<p><strong>The overhead is not a constant.</strong> Across the day's samples the classifier call\nranged from 476 ms to 1449 ms, and the first request after a restart cost 1449 ms\non its own. It is a full inference call and inherits whatever the backend is\ndoing. Any single number you quote for it is the number you happened to catch.</p>\n<p><strong>Token cost is fixed and larger than you'd guess.</strong> Each classification sent\n<strong>281–293 input tokens</strong> — the rubric — and returned about 10. Routing <code>hi</code>, a\none-token prompt, costs ~281 tokens of classification before anything answers.</p>\n<p><strong>The serving call slows down too.</strong> 456–806 ms when routed, against 495 ms\ndirect for the identical request. The classifier appears to leave the backend\nbusy for the request queued behind it. You would never see this from wall clock\nalone.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Measure the parts, not the total</div><div class=\"admonitionContent_BuS1\"><p>Paired wall-clock timing tells you <em>that</em> it got slower, never <em>where</em>. Decompose\ninto the classifier call and the serving call before you quote a figure — the\nfirst honest number here was almost double the one a single <code>time</code> run suggested.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-trap-worth-knowing-about\">The trap worth knowing about<a href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/#the-trap-worth-knowing-about\" class=\"hash-link\" aria-label=\"Direct link to The trap worth knowing about\" title=\"Direct link to The trap worth knowing about\" translate=\"no\">​</a></h2>\n<p>If the classification call times out, returns the wrong shape, or comes back\nempty, the router falls back to <strong>the heuristic scorer</strong> — the thing whose\nfailures you just paid half a second to avoid. Silently.</p>\n<p>The official docs name the way out: set <code>classifier_fallback: default_model</code> and\na timeout routes to a model you chose deliberately, rather than to the scorer\nthat sends hard questions to a 3B model.</p>\n<p>Also worth checking rather than assuming: the docs give <code>timeout_ms</code> a default of\n2000, while the package in our container defaults to 3000. Read the version you\nactually installed.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"so-which-one\">So which one<a href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/#so-which-one\" class=\"hash-link\" aria-label=\"Direct link to So which one\" title=\"Direct link to So which one\" translate=\"no\">​</a></h2>\n<table><thead><tr><th></th><th>Heuristic</th><th>LLM classifier</th></tr></thead><tbody><tr><td>Latency added</td><td>sub-millisecond</td><td>476–1449 ms</td></tr><tr><td>Extra API calls</td><td>none</td><td>one per request</td></tr><tr><td>Extra input tokens</td><td>none</td><td>~280 per request</td></tr><tr><td>Hard-but-short prompts</td><td>routed down</td><td>routed correctly</td></tr><tr><td>Easy prompts</td><td>routed correctly</td><td>routed up</td></tr><tr><td>Fails by</td><td>being confidently wrong</td><td>timing out, then being confidently wrong</td></tr></tbody></table>\n<p>The heuristic is the right default for high-volume traffic that looks alike, and\nyou can improve it a lot with <code>custom_technical_keywords</code> for your own domain\nvocabulary. The LLM classifier earns its cost when prompts are varied, when\ngetting the model wrong is expensive, and when half a second doesn't matter —\nbatch work, agents, anything already taking seconds.</p>\n<p>What neither of them is, is free.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting\">Troubleshooting<a href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/#troubleshooting\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting\" title=\"Direct link to Troubleshooting\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-response-says-smart-router-instead-of-a-model-name\">The response says <code>smart-router</code> instead of a model name<a href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/#the-response-says-smart-router-instead-of-a-model-name\" class=\"hash-link\" aria-label=\"Direct link to the-response-says-smart-router-instead-of-a-model-name\" title=\"Direct link to the-response-says-smart-router-instead-of-a-model-name\" translate=\"no\">​</a></h3>\n<p><code>return_raw_model_name</code> isn't set. Without it the router echoes the alias you\nasked for and you can't tell what served the request.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"every-prompt-routes-to-the-same-tier\">Every prompt routes to the same tier<a href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/#every-prompt-routes-to-the-same-tier\" class=\"hash-link\" aria-label=\"Direct link to Every prompt routes to the same tier\" title=\"Direct link to Every prompt routes to the same tier\" translate=\"no\">​</a></h3>\n<p>Check the <code>signals</code> field in the routing decision. If it's <code>null</code>, no dimension\nmatched at all and the prompt scored 0.00, which lands in SIMPLE. That's a sign\nyour traffic doesn't use the vocabulary the default keyword lists expect.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"routing-decisions-arent-in-the-spend-log\">Routing decisions aren't in the spend log<a href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/#routing-decisions-arent-in-the-spend-log\" class=\"hash-link\" aria-label=\"Direct link to Routing decisions aren't in the spend log\" title=\"Direct link to Routing decisions aren't in the spend log\" translate=\"no\">​</a></h3>\n<p>They're on the routed request, under <code>metadata.routing_decision</code>, not on the\nclassifier call. Filter with <code>map(select(.))</code> as above — half the rows are the\nclassifier calls themselves and carry no decision.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-first-request-after-a-restart-is-much-slower\">The first request after a restart is much slower<a href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/#the-first-request-after-a-restart-is-much-slower\" class=\"hash-link\" aria-label=\"Direct link to The first request after a restart is much slower\" title=\"Direct link to The first request after a restart is much slower\" translate=\"no\">​</a></h3>\n<p>Cold start. The first classification here took 1449 ms against roughly 550 ms\nonce warm. Discard the first sample when timing.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Route work to the right model</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>The gateway now decides which model runs, and you know what that decision costs\nin latency and tokens. It still forwards every prompt verbatim to whichever model\nwins — including the ones containing customer names, emails and API keys.</p>\n<p>The next post puts a filter in that path.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.litellm.ai/docs/proxy/auto_routing\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Auto Routing — configuration reference</a></li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/\">Part 1 — deploy an LLM gateway on a WEC Instance</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/",
            "title": "LiteLLM complexity routing: the right model for each request, and what it costs in latency",
            "summary": "How LiteLLM's complexity router decides which model answers a request — the seven scoring dimensions, the arithmetic on a real prompt, and a measured comparison of the free keyword scorer against an LLM classifier.",
            "date_modified": "2026-08-19T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "llm",
                "gateway",
                "routing",
                "cost",
                "self-hosting",
                "litellm",
                "wec"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/",
            "content_html": "<div class=\"tutorialHero\"><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/litellm-wordmark-86d6f0432720b27534e05bc579dfc29c.png\" alt=\"LiteLLM\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 5 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->5</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->5<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting an LLM gateway</div><ul class=\"skillTracker__steps\"><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">1</span><span class=\"skillTracker__skill\" data-state=\"current\">One endpoint, scoped keys</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/litellm-complexity-routing/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Route work to the right model</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mask-pii-llm-gateway-presidio/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Mask PII at the gateway</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mask-logs-not-responses-langfuse-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Clean traces, untouched answers</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/load-test-llm-gateway-blocking-callback/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove it holds under load</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Here's how it usually goes. One app needs a model, so you paste the API key into\nits <code>.env</code>. Then a second app needs one. Then a script. Six months later the same\nkey is in five places, nobody remembers which of them is still running, and you\ncan't rotate it without breaking something you'll only find out about when it\nbreaks.</p>\n<p>A gateway is the boring fix. One endpoint in front of every model, one place that\nholds the real credential, and a scoped key per app that you can revoke on its own.\nThis post deploys one on a WEC Instance and points it at the WEC Inference API.</p>\n<!-- -->\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-were-putting-on-the-box\">What we're putting on the box<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#what-were-putting-on-the-box\" class=\"hash-link\" aria-label=\"Direct link to What we're putting on the box\" title=\"Direct link to What we're putting on the box\" translate=\"no\">​</a></h2>\n<p>The host already runs five other stacks, which matters — this is the normal case,\nnot a clean VM. Before adding anything, check what's listening and what's free:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--format</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{{.Names}}\\t{{.Ports}}'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> ss </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-tlnp</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-E</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">':(4000|5432|5433)\\b'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langfuse-postgres-1   127.0.0.1:5433-&gt;5432/tcp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langfuse-clickhouse-1 127.0.0.1:8123-&gt;8123/tcp, 127.0.0.1:9000-&gt;9000/tcp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">openclaw-caddy-1      100.87.239.229:80-&gt;80/tcp, 100.87.239.229:443-&gt;443/tcp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">LISTEN 0  244   127.0.0.1:5432   users:((\"postgres\",pid=892))</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">LISTEN 0  4096  127.0.0.1:5433   users:((\"docker-proxy\"))</span><br></div></code></pre></div></div>\n<p>Port 4000 is free. Postgres 5432 belongs to the host and 5433 to Langfuse, so the\ngateway's database gets neither — it won't publish a port at all.</p>\n<p>Then confirm the backend answers before putting anything in front of it:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> https://inference.wiline.com/v1/models </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$WEC_API_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.data[].id'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">whisper-large-v3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">gemma4</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">wiline-coding</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">zai-org/GLM-5.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen3.5:9B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen3.5-122B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">kokoro</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Llama3.1-8B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">bge-m3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen3.5-9B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">wiline-auto</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">wiline-cost</span><br></div></code></pre></div></div>\n<p>Thirteen models on one key. That's the thing we're about to stop handing out.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-stack\">The stack<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#the-stack\" class=\"hash-link\" aria-label=\"Direct link to The stack\" title=\"Direct link to The stack\" translate=\"no\">​</a></h2>\n<p>Two containers: the gateway and a Postgres for its own state. Make a directory and\nwrite the compose file:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> ~/llm-gateway </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/llm-gateway</span><br></div></code></pre></div></div>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.yml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">services</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">litellm</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> ghcr.io/berriai/litellm</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">main</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">stable</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">container_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> llm</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">gateway</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">restart</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> unless</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">stopped</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">ports</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"127.0.0.1:4000:4000\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">volumes</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./config.yaml</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/app/config.yaml</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">ro</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">command</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"--config\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"/app/config.yaml\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"--port\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"4000\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">env_file</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> .env</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">depends_on</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">db</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">condition</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> service_healthy</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">db</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> postgres</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">17</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">container_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> llm</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">gateway</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">db</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">restart</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> unless</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">stopped</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">user</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"999:999\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">environment</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">POSTGRES_USER</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> llmproxy</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">POSTGRES_PASSWORD</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">POSTGRES_PASSWORD</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">POSTGRES_DB</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> litellm</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">volumes</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> gateway</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">db</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/var/lib/postgresql/data</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">healthcheck</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">test</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"CMD-SHELL\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"pg_isready -U llmproxy -d litellm\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">interval</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> 5s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">timeout</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> 5s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">retries</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">10</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token key atrule\" style=\"color:#00a4db\">volumes</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  gateway</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">db</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><br></div></code></pre></div></div>\n<p>Four of those lines are doing real work:</p>\n<p><code>127.0.0.1:4000:4000</code> keeps the gateway off the public internet. It holds every\nmodel credential you own, so publishing it to <code>0.0.0.0</code> would put all of them\nbehind a single shared password. <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\">Part 1 of the hardening\nseries</a> is the long version of why\nthat's a bad trade.</p>\n<p>The database has no <code>ports:</code> at all. Nothing outside the compose network needs to\nreach it, and 5432 was taken anyway.</p>\n<p><code>user: \"999:999\"</code> runs Postgres as its built-in non-root user from the first boot.\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/\">Part 2</a> covers what that buys you\nand how to find the right UID for an image instead of guessing.</p>\n<p><code>condition: service_healthy</code> matters more than it looks. The gateway runs database\nmigrations on startup — without it, it races Postgres and crash-loops.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"telling-it-about-the-models\">Telling it about the models<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#telling-it-about-the-models\" class=\"hash-link\" aria-label=\"Direct link to Telling it about the models\" title=\"Direct link to Telling it about the models\" translate=\"no\">​</a></h2>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">model_list</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">model_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> qwen</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">small</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_params</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">model</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> openai/Qwen2.5</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">3B</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">api_base</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> https</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">//inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">api_key</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> os.environ/WEC_API_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">model_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> qwen</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">mid</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_params</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">model</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> openai/Qwen3.5</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">9B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">api_base</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> https</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">//inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">api_key</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> os.environ/WEC_API_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">model_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> qwen</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">large</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_params</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">model</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> openai/Qwen3.5</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">122B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">api_base</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> https</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">//inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">api_key</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> os.environ/WEC_API_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">model_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> embeddings</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_params</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">model</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> openai/bge</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">m3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">api_base</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> https</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">//inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">api_key</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> os.environ/WEC_API_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token key atrule\" style=\"color:#00a4db\">general_settings</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">master_key</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> os.environ/LITELLM_MASTER_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">database_url</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> os.environ/DATABASE_URL</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">store_model_in_db</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:#36acaa\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token key atrule\" style=\"color:#00a4db\">litellm_settings</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">drop_params</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:#36acaa\">true</span><br></div></code></pre></div></div>\n<p><code>model_name</code> is the alias your apps ask for. <code>model:</code> is what the gateway actually\ncalls. That indirection is most of the value — swap <code>qwen-mid</code> to point somewhere\nelse next month and not one client changes.</p>\n<p>The <code>openai/</code> prefix says \"speak the OpenAI protocol to a custom <code>api_base</code>.\" The\nWEC Inference API is OpenAI-compatible, so that's all it takes.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"secrets\">Secrets<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#secrets\" class=\"hash-link\" aria-label=\"Direct link to Secrets\" title=\"Direct link to Secrets\" translate=\"no\">​</a></h2>\n<p>Generate the passwords rather than inventing them:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">PGPW</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable\" style=\"color:#36acaa\">openssl rand </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-hex</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable number\" style=\"color:#36acaa\">16</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">MK</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"sk-</span><span class=\"token string variable\" style=\"color:#36acaa\">$(</span><span class=\"token string variable\" style=\"color:#36acaa\">openssl rand </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-hex</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable number\" style=\"color:#36acaa\">20</span><span class=\"token string variable\" style=\"color:#36acaa\">)</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">POSTGRES_PASSWORD=</span><span class=\"token string variable\" style=\"color:#36acaa\">${PGPW}</span><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">DATABASE_URL=postgresql://llmproxy:</span><span class=\"token string variable\" style=\"color:#36acaa\">${PGPW}</span><span class=\"token string\" style=\"color:#e3116c\">@db:5432/litellm</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">LITELLM_MASTER_KEY=</span><span class=\"token string variable\" style=\"color:#36acaa\">${MK}</span><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">WEC_API_KEY=your-wec-key-here</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">chmod</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">600</span><span class=\"token plain\"> .env</span><br></div></code></pre></div></div>\n<p>Then put your real WEC key in place of <code>your-wec-key-here</code>. The master key is the\ngateway's root credential — it can create keys, delete keys and reach every model,\nso it goes in your password manager and nowhere else.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"bringing-it-up\">Bringing it up<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#bringing-it-up\" class=\"hash-link\" aria-label=\"Direct link to Bringing it up\" title=\"Direct link to Bringing it up\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> ✔ Image ghcr.io/berriai/litellm:main-stable Pulled          58.2s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> ✔ Network llm-gateway_default               Created          0.1s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> ✔ Volume llm-gateway_gateway-db             Created          0.1s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> ✔ Container llm-gateway-db                  Healthy          8.8s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> ✔ Container llm-gateway                     Created          0.2s</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker compose up pulling the image and creating both containers\" src=\"https://development-wec.wiline.com/docs/assets/images/gw-compose-up-e741e826f043d86aa7f1a7214f71fbcb.png\" width=\"1066\" height=\"298\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> One pull, two containers, a network and a volume.</p>\n<p><code>Created</code> rather than <code>Started</code> on that last line is normal for the compose output,\nbut check anyway — and check what the image cost you in disk while you're there:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-a</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">df</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-h</span><span class=\"token plain\"> /</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">NAME             IMAGE                                 SERVICE   STATUS                   PORTS</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">llm-gateway      ghcr.io/berriai/litellm:main-stable   litellm   Up 3 minutes             127.0.0.1:4000-&gt;4000/tcp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">llm-gateway-db   postgres:17                           db        Up 3 minutes (healthy)   5432/tcp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Filesystem      Size  Used Avail Use% Mounted on</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">/dev/vda1        58G   54G  4.2G  93% /</span><br></div></code></pre></div></div>\n<p>Note the database's <code>PORTS</code> column — <code>5432/tcp</code> with no host binding in front of\nit. The gateway image is 1.16GB, which on a box already running five stacks is not\nnothing. Check you have the room before you start, not after.</p>\n<p>Two lines in the startup log are worth reading, because both look worse than they are:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose logs litellm </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--tail</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">40</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">✅ Migration diff applied successfully</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INFO:     Application startup complete.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INFO:     Uvicorn running on http://0.0.0.0:4000 (Press CTRL+C to quit)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">register_model: model=openai/Qwen2.5-3B-Instruct not in built-in cost map and no</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">prefix/region variant matched; cache cost fields will default to 0.</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Container status, disk usage and the startup log with migrations and the cost-map warnings\" src=\"https://development-wec.wiline.com/docs/assets/images/gw-ps-logs-ed50299979281a4fb3dfd6ec9c89d189.png\" width=\"2496\" height=\"1408\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> Migrations applied, application startup complete, and one cost-map\nwarning per model.</p>\n<p><code>0.0.0.0:4000</code> is the bind <em>inside</em> the container. The host publish is still\n<code>127.0.0.1</code>, which is what actually gates access. And the cost-map warning is\nnarrower than it reads — it's about cache pricing specifically. Regular token\npricing still happens, which turns out to matter later.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-first-call\">The first call<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#the-first-call\" class=\"hash-link\" aria-label=\"Direct link to The first call\" title=\"Direct link to The first call\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-a</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">.</span><span class=\"token plain\"> ./.env </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> +a</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-mid\",\"messages\":[{\"role\":\"user\",\"content\":\"Reply with exactly: gateway works\"}],\"max_tokens\":20}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">\"finish_reason\": \"length\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">\"message\": {</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"reasoning_content\": \"Thinking Process:\\n\\n1.  **Analyze the Request:**\\n    *   Input: \\\"\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"content\": null</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">\"usage\": {\"completion_tokens\": 20, \"prompt_tokens\": 16, \"total_tokens\": 36}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Raw response with finish_reason length, reasoning_content populated and content null\" src=\"https://development-wec.wiline.com/docs/assets/images/gw-reasoning-null-2d60fce2efcb8e175da27120082ac585.png\" width=\"2480\" height=\"1094\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> A successful call with nothing in it.</p>\n<p><code>content: null</code>. The call worked — it routed, it hit the backend, it counted tokens\n— but there's no answer in it.</p>\n<p><code>Qwen3.5-9B</code> is a reasoning model. It spent all twenty tokens thinking and had none\nleft to speak with. The obvious move is to give it more room:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-mid\",\"messages\":[{\"role\":\"user\",\"content\":\"Reply with exactly: gateway works\"}],\"max_tokens\":200}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'.choices[0].message.content, .usage'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">null</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">{</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"completion_tokens\": 200,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"prompt_tokens\": 16,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"total_tokens\": 216</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">}</span><br></div></code></pre></div></div>\n<p>Two hundred tokens, still nothing. And this is the part worth pausing on: an earlier\nrun of that exact command <em>did</em> answer, at 192 tokens. Same model, same prompt,\ndifferent amount of thinking. There's no <code>max_tokens</code> you can set that reliably\nbuys you an answer, because the reasoning length isn't fixed.</p>\n<p>Now the same prompt through the small model, at the original twenty tokens:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-small\",\"messages\":[{\"role\":\"user\",\"content\":\"Reply with exactly: gateway works\"}],\"max_tokens\":20}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'.choices[0].message.content, .usage'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">\"gateway works\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">{</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"completion_tokens\": 3,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"prompt_tokens\": 35,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"total_tokens\": 38</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Reasoning model burning 200 completion tokens and returning null, small model answering in 3\" src=\"https://development-wec.wiline.com/docs/assets/images/gw-token-contrast-922bab5aa94a6417e02dbc0d6fe0c874.png\" width=\"1854\" height=\"608\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 4.</strong> Two hundred tokens and no answer, against three tokens and the answer.</p>\n<p>Nothing is broken here. The reasoning model is doing exactly what it's built to do,\non a question that didn't need it. The fix isn't a bigger token budget — it's not\nsending trivial work to that model in the first place. Which is the entire argument\nfor routing, and the subject of the next post.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can put a gateway in front of several models, give each one an alias your apps\ncall instead of a provider model ID, and read a response well enough to tell a\nreasoning model from a plain one.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"proving-the-door-is-locked\">Proving the door is locked<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#proving-the-door-is-locked\" class=\"hash-link\" aria-label=\"Direct link to Proving the door is locked\" title=\"Direct link to Proving the door is locked\" translate=\"no\">​</a></h2>\n<p>A gateway that holds every credential you own should refuse anyone without a key.\nWorth checking rather than assuming:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"no key:     %{http_code}</span><span class=\"token string entity\" style=\"color:#36acaa\">\\n</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-mid\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"wrong key:  %{http_code}</span><span class=\"token string entity\" style=\"color:#36acaa\">\\n</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer sk-not-a-real-key\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-mid\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"master key: %{http_code}</span><span class=\"token string entity\" style=\"color:#36acaa\">\\n</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-mid\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":5}'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">no key:     401</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">wrong key:  401</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">master key: 200</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Three curl calls returning 401, 401 and 200\" src=\"https://development-wec.wiline.com/docs/assets/images/gw-auth-401-a9d9d89ff8432a6ec7a29486800ea82c.png\" width=\"1854\" height=\"302\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 5.</strong> Anonymous and forged both rejected, the real key through.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-admin-ui\">The admin UI<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#the-admin-ui\" class=\"hash-link\" aria-label=\"Direct link to The admin UI\" title=\"Direct link to The admin UI\" translate=\"no\">​</a></h2>\n<p>The gateway ships a web interface, and it's bound to localhost — which is the\npoint, so reach it over SSH rather than opening a port:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">ssh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-L</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">4000</span><span class=\"token plain\">:127.0.0.1:4000 ubuntu@your-wec-instance</span><br></div></code></pre></div></div>\n<p>Leave that running and open <code>http://localhost:4000/ui</code>. Username <code>admin</code>, password\nis the master key from your <code>.env</code>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The gateway&amp;#39;s login screen, asking for admin plus the master key\" src=\"https://development-wec.wiline.com/docs/assets/images/gw-ui-login-94d370f9590bd022684fb80ccabb2aff.png\" width=\"2508\" height=\"1458\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 6.</strong> The login page states its own default credentials, which is a good\nreminder that the master key is the only thing standing there.</p>\n<p>Click <strong>Models + Endpoints</strong>:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Model management listing qwen-small, qwen-mid, qwen-large and embeddings, all showing zero cost\" src=\"https://development-wec.wiline.com/docs/assets/images/gw-ui-models-4372c39b1bfb45a2d61782e145f1c5e2.png\" width=\"2510\" height=\"1464\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 7.</strong> All four aliases registered, each mapped to its <code>openai/...</code> target.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"keys-that-cant-do-everything\">Keys that can't do everything<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#keys-that-cant-do-everything\" class=\"hash-link\" aria-label=\"Direct link to Keys that can't do everything\" title=\"Direct link to Keys that can't do everything\" translate=\"no\">​</a></h2>\n<p>The master key can reach every model and create more keys. No application should\never hold it. Instead, issue a virtual key scoped to what that app actually needs.</p>\n<p><strong>Virtual Keys</strong> → <strong>+ Create New Key</strong>:</p>\n<table><thead><tr><th>Field</th><th>Value</th></tr></thead><tbody><tr><td>Owned By</td><td>You</td></tr><tr><td>Team</td><td>leave empty</td></tr><tr><td>Key Name</td><td><code>demo-app</code></td></tr><tr><td>Models</td><td><code>qwen-small</code></td></tr><tr><td>Max Budget (USD)</td><td><code>0.10</code></td></tr><tr><td>Reset Budget</td><td>daily</td></tr></tbody></table>\n<p>Two things about this form. <strong>Max Budget</strong> lives under <strong>Optional Settings</strong>, which\nstarts collapsed — easy to miss and then wonder where the budget field went. And\nleaving <strong>Models</strong> empty means <em>all models</em>, the opposite of what you want here.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The key creation form with qwen-small selected and a ten cent daily budget\" src=\"https://development-wec.wiline.com/docs/assets/images/gw-key-create-f440427f7d6f55ad01482ed469ff3624.png\" width=\"2510\" height=\"1460\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 8.</strong> Scoped to one model, capped at ten cents a day.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Save your Key dialog, warning that the key cannot be viewed again\" src=\"https://development-wec.wiline.com/docs/assets/images/gw-key-saved-e13c0abfcd4b4c0bfefa791764eaae50.png\" width=\"2560\" height=\"1476\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 9.</strong> The dialog says it plainly. Believe it.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Copy the key now</div><div class=\"admonitionContent_BuS1\"><p>The generated key is shown once and never again. Close this without copying and\nyour only option is to delete the key and create another.</p></div></div>\n<p>Now the part that justifies the whole exercise. Same key, two models:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$VK</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-small\",\"messages\":[{\"role\":\"user\",\"content\":\"say hi\"}],\"max_tokens\":20}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.choices[0].message.content'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Hi there! How can I assist you today?</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$VK</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"qwen-large\",\"messages\":[{\"role\":\"user\",\"content\":\"say hi\"}],\"max_tokens\":20}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">{</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"error\": {</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    \"message\": \"key not allowed to access model. This key can only access models=['qwen-small']. Tried to access qwen-large\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    \"type\": \"key_model_access_denied\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    \"param\": \"model\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    \"code\": \"403\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The same virtual key answering on qwen-small and being refused with a 403 on qwen-large\" src=\"https://development-wec.wiline.com/docs/assets/images/gw-key-403-ff8ff7ee956b8b081dd67c4d3834ac10.png\" width=\"1858\" height=\"508\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 10.</strong> One credential, one model. Leak it and the blast radius is that one line.</p>\n<p>Compare that to the situation we started in: a key in five <code>.env</code> files, with access\nto all thirteen models and no way to tell which app is using it.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can issue a scoped credential per application, restrict it to specific models\nwith a spend cap, and revoke it on its own without touching anything else.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-the-gateway-recorded\">What the gateway recorded<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#what-the-gateway-recorded\" class=\"hash-link\" aria-label=\"Direct link to What the gateway recorded\" title=\"Direct link to What the gateway recorded\" translate=\"no\">​</a></h2>\n<p>Every call lands in a spend log:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://127.0.0.1:4000/spend/logs </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'[.[] | select(.spend &gt; 0)] | .[-3:] | .[] | {model, spend, prompt_tokens, completion_tokens}'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">{</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"model\": \"openai/Qwen3.5-9B\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"spend\": 8.520000000000001e-05,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"prompt_tokens\": 16,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"completion_tokens\": 20</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">{</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"model\": \"openai/Qwen3.5-9B\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"spend\": 2.62e-05,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"prompt_tokens\": 11,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"completion_tokens\": 5</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Spend log rows showing token counts and a dollar figure per call\" src=\"https://development-wec.wiline.com/docs/assets/images/gw-spend-logs-dfdc834679299ecd1fcfcc774e83b5b3.png\" width=\"1856\" height=\"684\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 11.</strong> Tokens counted per call, with a price attached to each.</p>\n<p>That <code>select(.spend &gt; 0)</code> isn't cosmetic. Failed calls are logged too, at zero\ntokens and zero spend — the 401s from earlier are all sitting in there with\n<code>status: \"failure\"</code>. Useful on its own: the spend log doubles as a record of\nsomeone trying keys that don't work.</p>\n<p>Those dollar figures are real. They match the published per-model pricing for the\nAPI this gateway is calling — $0.70 per million input tokens and $3.70 per million\noutput for <code>Qwen3.5-9B</code>, the same rates you see listed against each model before\nyou pick one. So the spend log is a genuine record of what every call cost, broken\ndown per key and per model, from the first request.</p>\n<p>Two more things in the full log entry, both useful:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token property\" style=\"color:#36acaa\">\"litellm_overhead_time_ms\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">36.189</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token property\" style=\"color:#36acaa\">\"messages\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token property\" style=\"color:#36acaa\">\"response\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>The gateway measures its own overhead and reports it per request — 36ms here, which\nis a real number you can hold it to. And prompts and responses aren't stored by\ndefault: the log knows a call happened and what it cost, not what was said.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting\">Troubleshooting<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#troubleshooting\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting\" title=\"Direct link to Troubleshooting\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-gateway-container-starts-and-immediately-exits\">The gateway container starts and immediately exits<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#the-gateway-container-starts-and-immediately-exits\" class=\"hash-link\" aria-label=\"Direct link to The gateway container starts and immediately exits\" title=\"Direct link to The gateway container starts and immediately exits\" translate=\"no\">​</a></h3>\n<p>Almost always the database. Check <code>docker compose logs litellm</code> for migration\nerrors — if the gateway came up before Postgres was ready, the <code>depends_on</code>\ncondition is missing or the healthcheck isn't passing.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"content-comes-back-null-with-finish_reason-length\"><code>content</code> comes back <code>null</code> with <code>finish_reason: \"length\"</code><a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#content-comes-back-null-with-finish_reason-length\" class=\"hash-link\" aria-label=\"Direct link to content-comes-back-null-with-finish_reason-length\" title=\"Direct link to content-comes-back-null-with-finish_reason-length\" translate=\"no\">​</a></h3>\n<p>The model is a reasoning model and it used your whole token budget thinking. Raising\n<code>max_tokens</code> helps but doesn't guarantee anything — the same prompt at 200 tokens\nanswered on one run and returned <code>null</code> on the next. For work that doesn't need\nreasoning, send it to a model that doesn't do any.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-admin-ui-wont-load\">The admin UI won't load<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#the-admin-ui-wont-load\" class=\"hash-link\" aria-label=\"Direct link to The admin UI won't load\" title=\"Direct link to The admin UI won't load\" translate=\"no\">​</a></h3>\n<p>It's bound to <code>127.0.0.1</code>, so it's unreachable from anywhere but the box itself.\nUse an SSH tunnel. If the tunnel is up and the page still won't load, confirm the\nport in <code>docker compose ps</code> matches the one you forwarded.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"a-shell-chain-quietly-did-nothing\">A shell chain quietly did nothing<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#a-shell-chain-quietly-did-nothing\" class=\"hash-link\" aria-label=\"Direct link to A shell chain quietly did nothing\" title=\"Direct link to A shell chain quietly did nothing\" translate=\"no\">​</a></h3>\n<p>Chains joined with <code>&amp;&amp;</code> stop at the first command that fails, and a failed <code>grep</code>\ncounts as a failure. Sourcing an env file whose variable name you guessed wrong\nleaves a placeholder in place and everything looks fine until an auth error three\nsteps later. Check for placeholders explicitly:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'your-wec-key-here'</span><span class=\"token plain\"> .env</span><br></div></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"deleting-a-virtual-key\">Deleting a virtual key<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#deleting-a-virtual-key\" class=\"hash-link\" aria-label=\"Direct link to Deleting a virtual key\" title=\"Direct link to Deleting a virtual key\" translate=\"no\">​</a></h3>\n<p>From the terminal, by alias:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST http://127.0.0.1:4000/key/delete </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$LITELLM_MASTER_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"key_aliases\":[\"demo-app\"]}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq</span><br></div></code></pre></div></div>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>One endpoint, scoped keys</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>You now have one endpoint, a credential per app, and a log of every call. The\nobvious question is the one Figure 4 raised: if the small model answers in 3 tokens\nwhat the reasoning model spends 200 on without answering at all, why is anything\nrouting to the expensive one by default?</p>\n<p>The next post puts the gateway in charge of that decision, and measures what asking\nit costs.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.docker.com/reference/compose-file/services/#depends_on\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker Compose — service dependencies and healthchecks</a></li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\">Harden Docker networks for multi-agent hosts</a></li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/\">Root by default: hardening container privilege</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/deploy-llm-gateway-wec-instance/",
            "title": "One endpoint, many models: deploy an LLM gateway on a WEC Instance",
            "summary": "Every app on a box holding the same API key is a problem waiting to happen. A gateway fixes that — one endpoint, scoped keys per app, per-key budgets, and a log of who spent what. Deployed for real on a host already running five other stacks, including the parts that surprised us.",
            "date_modified": "2026-08-13T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "llm",
                "gateway",
                "docker",
                "self-hosting",
                "api",
                "litellm",
                "wec"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/",
            "content_html": "<div class=\"tutorialHero\"><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2255\" height=\"527\" fill=\"none\" viewBox=\"0 0 2255 527\" class=\"tutorialHero__langfuse\"><path fill=\"#1B1917\" d=\"M652.669 116.433c0-10.261-7.683-17.956-17.926-17.956H607V60h87.923v316.366h-42.254zM804.056 379.786c-39.693 0-72.131-27.362-72.131-68.831 0-41.042 28.596-69.686 84.935-69.686h39.693c7.256 0 12.805-5.558 12.805-12.826v-8.55c0-25.651-20.487-38.905-40.547-38.905-18.353 0-33.291 8.123-41.828 26.934h-45.668c11.95-44.462 46.095-65.41 88.349-65.41 40.547 0 82.374 22.231 82.374 77.808v156.046h-42.68v-14.109c0-5.13-5.549-7.695-9.817-4.702-16.646 11.97-32.438 22.231-55.485 22.231m5.975-38.477c18.78 0 34.572-9.406 52.498-27.362 4.695-4.702 6.829-10.688 6.829-17.1v-6.413c0-7.268-5.549-12.826-12.805-12.826H815.58c-27.743 0-40.974 12.398-40.974 31.637 0 17.956 12.377 32.064 35.425 32.064M961.855 376.366V147.642h42.255v19.666c0 5.13 6.4 6.84 10.24 2.992 13.23-12.825 31.59-27.788 60.61-27.788 36.71 0 70.85 23.086 70.85 75.671v158.183h-42.25V227.161c0-29.499-17.93-45.745-39.7-45.745-20.91 0-35 11.971-49.93 29.499-7.26 8.978-9.82 19.238-9.82 30.354v135.097zM1287.48 467c-52.5 0-85.79-24.796-95.61-63.273h46.1c7.25 15.818 20.06 25.651 45.67 25.651 34.14 0 55.48-20.093 55.48-65.838v-7.268c0-5.13-4.27-7.695-9.39-3.42-14.08 12.398-32.01 20.093-49.08 20.093-58.05 0-96.46-44.889-96.46-115.003 0-70.113 44.39-115.43 98.17-115.43 15.79 0 30.3 4.275 44.38 14.963 5.98 4.275 12.38.855 12.38-5.986v-3.847h42.26V363.54c0 72.678-43.97 103.46-93.9 103.46m-2.56-132.959c19.2 0 33.29-8.55 44.81-20.949 7.26-8.122 9.39-13.68 9.39-26.506v-61.563c0-12.826-2.13-20.948-10.67-29.071-9.39-8.978-22.62-14.964-39.69-14.964-35 0-61.89 29.072-61.89 76.954 0 47.883 24.76 76.099 58.05 76.099M1455.92 199.372c0-7.268-5.97-13.253-13.23-13.253h-32.44v-38.477h32.44c7.26 0 13.23-5.985 13.23-13.253v-5.986c0-45.744 23.48-68.403 69.15-68.403h29.02v38.477h-29.45c-17.5 0-26.46 9.833-26.46 29.926v5.986c0 7.268 5.97 13.253 13.23 13.253h42.68v38.477h-42.68c-7.26 0-13.23 5.985-13.23 13.253v176.994h-42.26zM1652.02 381.496c-35.85 0-69.14-23.086-69.14-75.671V147.642h42.25v150.06c0 29.499 17.07 44.889 37.13 44.889 21.77 0 35.85-11.97 50.79-29.499 7.26-8.977 9.82-19.238 9.82-30.354V147.642h42.25v228.724h-42.25V356.7c0-5.131-6.4-6.841-10.24-2.993-13.24 12.826-31.59 27.789-60.61 27.789M1893.57 381.496c-38.84 0-79.39-19.239-90.06-65.838h43.54c6.4 17.528 23.9 29.498 44.81 29.498 23.05 0 37.13-13.68 37.13-30.353 0-16.246-11.09-25.224-28.59-30.354l-36.28-10.261c-31.58-8.978-55.06-29.499-55.06-64.556 0-38.049 35.43-67.12 75.55-67.12 32.01 0 70.85 14.535 81.09 65.41h-40.55c-5.55-17.528-20.06-29.071-40.54-29.071-20.06 0-34.58 12.398-34.58 28.216 0 13.253 8.11 23.086 27.32 28.644l34.15 9.833c32.43 9.406 58.47 29.072 58.47 66.693 0 39.332-34.15 69.259-76.4 69.259M2098.54 381.496c-61.46 0-102.01-51.73-102.01-119.706s43.11-119.278 101.58-119.278c63.6 0 96.89 51.302 96.89 109.872v23.087h-144.26c-5.98 0-8.54 3.847-7.26 13.68 4.7 32.064 30.31 54.295 55.49 54.295 18.78 0 35-9.405 45.67-27.788h44.81c-16.22 40.187-49.94 65.838-90.91 65.838m43.11-141.51c6.83 0 9.39-3.42 7.68-14.108-4.69-26.506-24.33-45.317-51.22-45.317-25.6 0-47.37 18.811-54.2 45.745-2.56 9.833.85 13.68 6.83 13.68z\"></path><path fill=\"#FF5D5F\" d=\"m286.292 286.105 34.597 27.791s26.473-19.661 45.941-22.545c20.418-3.025 42.202 8.359 62.388 21.93 30.489 20.498 56.149 46.508 56.149 46.508l30.06-29.493s-82.879-89.795-148.597-81.672c-43.105 5.328-80.538 37.481-80.538 37.481\"></path><path fill=\"#4E9CFF\" d=\"M88.358 114.862 60 146.056s79.009 73.732 141.224 73.732c28.358 0 67.684-22.216 101.523-51.079 19.283-16.448 40.835-35.13 62.388-35.13 14.487 0 33.594 7.673 51.612 27.824 0 0 11.63-6.974 18.716-11.985 6.228-4.404 15.479-11.91 15.479-11.91-25.918-27.663-63.407-47.883-85.807-45.9-36.299.005-62.388 22.601-94.717 48.735s-45.94 36.907-69.194 36.907c-39.134 0-112.866-62.388-112.866-62.388M88.358 352.463 60 321.269s79.009-73.732 141.224-73.732c28.358 0 67.684 22.216 101.523 51.079 19.283 16.448 40.835 35.13 62.388 35.13 14.556 0 33.518-7.989 51.612-28.358 0 0 10.877 6.705 17.582 11.344 6.894 4.769 17.015 12.655 17.015 12.655-25.931 27.883-63.693 48.323-86.209 46.33-36.299-.005-57.851-19.24-90.179-45.374-32.329-26.133-50.478-40.268-73.732-40.268-39.134 0-112.866 62.388-112.866 62.388M458.142 185.149c-7.378 5.1-19.283 12.478-19.283 12.478s6.806 14.746 6.806 34.597-6.239 36.866-6.239 36.866 10.688 6.675 17.582 11.343c7.162 4.849 18.149 13.045 18.149 13.045s13.045-27.224 13.045-61.254-13.045-59.552-13.045-59.552-10.236 7.792-17.015 12.477\"></path><path fill=\"#FF5D5F\" d=\"m287.995 180.612 32.895-27.224s26.473 19.046 45.941 21.93c20.417 3.026 42.202-8.359 62.388-21.93 30.489-20.498 56.149-46.507 56.149-46.507l30.06 29.492s-82.879 89.795-148.597 81.672c-43.105-5.328-78.836-37.433-78.836-37.433M208.601 91c42.538 0 78.264 36.299 78.264 36.299s-9.941 7.832-16.448 13.045c-6.777 5.429-17.582 14.179-17.582 14.179s-18.711-19.851-44.234-19.851c-10.465 0-24.066 6.286-38.567 18.716-11.188 9.591-22.829 21.514-30.627 36.299-6.743 12.784-10.42 27.85-10.776 43.672-.446 19.873 6.597 40.704 18.149 57.283 7.743 11.112 16.983 19.474 26.657 26.657 12.555 9.322 25.648 15.881 35.164 15.881 10.166 0 19.306-3.533 26.09-6.806 10.776-6.239 19.278-13.612 19.278-13.612l33.463 27.791s-13.612 13.612-32.323 23.821c-12.091 5.963-27.632 11.91-46.508 11.91-18.862 0-40.767-10.022-61.254-25.522-13.244-10.021-26.225-21.895-36.298-36.299-16.51-23.607-25.017-52.328-24.96-81.104.057-29.136 9.451-57.993 26.094-81.672C138.273 117.657 176.86 91 208.601 91\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Lock down Docker networks</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">2</span><span class=\"skillTracker__skill\" data-state=\"current\">Run containers as non-root</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/\">Part 1</a> of this series audited a live\nmulti-agent box and found every network-exposure question worth asking. One line from\nthat audit didn't get followed up: <em>\"the Postgres, ClickHouse and Redis behind Langfuse\nwere published to <code>127.0.0.1</code> instead of the world — someone made a good decision\nthere. Hold that thought.\"</em></p>\n<p>Here's the other half of that thought: getting the network right says nothing about\nwhat happens <em>after</em> someone's already inside a container. If the process running in\nthere is root, a compromise starts with the keys to the whole filesystem. So we checked\n— on the same box, the same Langfuse stack Part 1 already praised — whether \"network\ncorrect\" also meant \"privilege correct.\" It didn't, for two of the six containers. Here's\nwhat fixing that actually looked like, including the part that broke.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"checking-whos-actually-root\">Checking who's actually root<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/#checking-whos-actually-root\" class=\"hash-link\" aria-label=\"Direct link to Checking who's actually root\" title=\"Direct link to Checking who's actually root\" translate=\"no\">​</a></h2>\n<p>Docker doesn't tell you this by default — a container can be published correctly,\nhealthy, and running as <code>root</code> the entire time, with nothing in <code>docker ps</code> hinting at\nit. The only way to know is to ask the container directly:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> langfuse-postgres-1 </span><span class=\"token function\" style=\"color:#d73a49\">id</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-u</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> langfuse-redis-1 </span><span class=\"token function\" style=\"color:#d73a49\">id</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-u</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">0</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Both id -u checks returning 0 — both containers running as root\" src=\"https://development-wec.wiline.com/docs/assets/images/privsec-root-confirmed-9af63f21bf8c07c7554a6cbab4f02929.png\" width=\"617\" height=\"96\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> The check that most setups never run: both <code>id -u</code> calls come back <code>0</code>.\nHealthy, published correctly, and root the entire time.</p>\n<p><code>0</code> is root. Both were running as root, and looking at the compose file explains why —\nneither service has a <code>user:</code> directive at all:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.yml (before)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">redis</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> docker.io/redis</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">7</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">restart</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> always</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">command</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">&gt;</span><span class=\"token scalar string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token scalar string\" style=\"color:#e3116c\">      --requirepass ${REDIS_AUTH:-myredissecret}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token scalar string\" style=\"color:#e3116c\">      --maxmemory-policy noeviction</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">postgres</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> docker.io/postgres</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">$</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">POSTGRES_VERSION</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">-17</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">restart</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> always</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">...</span><br></div></code></pre></div></div>\n<p>Nobody decided these should run as root. It's just what happens when you don't set\nanything — same shape as Part 1's finding that an unset Redis password defaults to no\nauthentication at all. The <code>clickhouse</code> service in the same file shows the fix already\nhalf-done, sitting right there for comparison:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.yml (already correct)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">clickhouse</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> docker.io/clickhouse/clickhouse</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">server</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">restart</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> always</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">user</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"101:101\"</span><br></div></code></pre></div></div>\n<p>One line. Somebody set it for ClickHouse and never got to the other two.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"finding-the-right-user-not-a-guess\">Finding the right user, not a guess<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/#finding-the-right-user-not-a-guess\" class=\"hash-link\" aria-label=\"Direct link to Finding the right user, not a guess\" title=\"Direct link to Finding the right user, not a guess\" translate=\"no\">​</a></h2>\n<p>Don't invent a UID — both official images already ship a non-root user built for exactly\nthis, and guessing wrong means either a permission error or, worse, a UID that silently\ndoesn't match the one the image's own files are owned by:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> langfuse-postgres-1 </span><span class=\"token function\" style=\"color:#d73a49\">id</span><span class=\"token plain\"> postgres</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> langfuse-redis-1 </span><span class=\"token function\" style=\"color:#d73a49\">id</span><span class=\"token plain\"> redis</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">uid=999(postgres) gid=999(postgres) groups=999(postgres),101(ssl-cert)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">uid=999(redis) gid=999(redis) groups=999(redis)</span><br></div></code></pre></div></div>\n<p>Both images happen to use <code>999:999</code> for their built-in non-root user. Add it to the\ncompose file:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.yml (after)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">redis</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> docker.io/redis</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">7</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">user</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"999:999\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">restart</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> always</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">postgres</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> docker.io/postgres</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">$</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">POSTGRES_VERSION</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">-17</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">user</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"999:999\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">restart</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> always</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">...</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-conversion-itself-was-a-non-event\">The conversion itself was a non-event<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/#the-conversion-itself-was-a-non-event\" class=\"hash-link\" aria-label=\"Direct link to The conversion itself was a non-event\" title=\"Direct link to The conversion itself was a non-event\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> --force-recreate postgres redis</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> ✔ Container langfuse-postgres-1 Recreated</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> ✔ Container langfuse-redis-1 Recreated</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">5</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> langfuse-postgres-1 </span><span class=\"token function\" style=\"color:#d73a49\">id</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-u</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> langfuse-redis-1 </span><span class=\"token function\" style=\"color:#d73a49\">id</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-u</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langfuse-postgres-1   Up 34 seconds (healthy)   127.0.0.1:5433-&gt;5432/tcp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langfuse-redis-1      Up 34 seconds (healthy)   127.0.0.1:6379-&gt;6379/tcp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">999</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">999</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker compose ps showing both containers healthy, and id -u now returning 999\" src=\"https://development-wec.wiline.com/docs/assets/images/privsec-nonroot-healthy-2aa6142e51931837aa80ee308e8be448.png\" width=\"1584\" height=\"384\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> Same two containers, seconds after <code>--force-recreate</code>: both <code>healthy</code>, both\n<code>id -u</code> now <code>999</code> instead of <code>0</code>.</p>\n<p>No permission errors, no ownership drama, both healthy within seconds. That's worth\nsaying plainly because it's not always true — if these volumes had been initialized as\nroot somewhere upstream and never <code>chown</code>-ed, this exact command can fail with\n<code>permission denied</code> on the data directory. It didn't here, but check your own logs after\nrecreating, don't assume \"no output\" means \"no problem.\"</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can check whether a running container is actually root (<code>docker exec &lt;container&gt; id -u</code>), find its image's intended non-root user instead of guessing a UID\n(<code>docker exec &lt;container&gt; id &lt;username&gt;</code>), and convert it with a one-line <code>user:</code> in\ncompose.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-part-that-actually-broke\">The part that actually broke<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/#the-part-that-actually-broke\" class=\"hash-link\" aria-label=\"Direct link to The part that actually broke\" title=\"Direct link to The part that actually broke\" translate=\"no\">​</a></h2>\n<p>Everything above was clean. Then the logs from a completely different container told a\ndifferent story:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> logs langfuse-langfuse-worker-1 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--tail</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">20</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Error: getaddrinfo ENOTFOUND redis</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    at GetAddrInfoReqWrap.onlookupall [as oncomplete] (node:dns:122:26)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Redis error getaddrinfo ENOTFOUND redis</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Redis error connect ECONNREFUSED 172.19.0.5:6379</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Queue job mixpanel-integration-processing-queue errored: Error: Socket timeout.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Expecting data, but didn't receive any in 30000ms.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Queue job trace-delete errored: Error: Socket timeout. Expecting data, but didn't</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">receive any in 30000ms.</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"langfuse-worker&amp;#39;s logs showing repeated 30-second socket timeouts against Redis, one per queue\" src=\"https://development-wec.wiline.com/docs/assets/images/privsec-worker-broken-e776f29e677755bd8a5e61864dbe22bc.png\" width=\"995\" height=\"428\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> A container nobody touched, failing anyway — every queue job erroring with\nthe same 30-second socket timeout against Redis, well after the recreate that caused it.</p>\n<p><code>langfuse-worker</code> never touched Postgres or Redis's ownership — it just holds a live\nconnection to Redis, and <code>--force-recreate</code> tore down the container that connection\npointed at. Docker's own healthcheck said Redis was <code>healthy</code> again well before the\nworker gave up retrying; ioredis just doesn't reconnect cleanly on its own here, and\nkept failing with 30-second socket timeouts long after the dependency it depended on was\nback.</p>\n<p>The fix is the dependent service's own restart, not another look at Redis:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose restart langfuse-worker langfuse-web</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">2026-08-10T19:40:22.505Z info    Redis connection has been closed.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">2026-08-10T19:40:22.769Z info    Shutdown complete, exiting process...</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">10</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> logs langfuse-langfuse-worker-1 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--tail</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">15</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">experiment-create-queue executor started: true</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">posthog-integration-queue executor started: true</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">data-retention-queue executor started: true</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">webhook-queue executor started: true</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Listening: http://21caf9a28775:3030</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"langfuse-worker&amp;#39;s logs after restart, showing every queue executor starting cleanly\" src=\"https://development-wec.wiline.com/docs/assets/images/privsec-worker-recovered-ca07f68b5e4cb0994524f20c497f41ca.png\" width=\"994\" height=\"372\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 4.</strong> Same container, one restart later — every queue executor starts clean, no\nRedis errors anywhere in the log.</p>\n<p>Clean queue startup, no Redis errors. That's the whole lesson: <strong>hardening a container\ndoesn't stay contained to that container.</strong> Anything holding a live connection to the\nthing you just recreated needs its own restart, on purpose, not as an afterthought you\ndiscover from an error log twenty minutes later.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>This is the one to remember</div><div class=\"admonitionContent_BuS1\"><p><code>--force-recreate</code> on a shared dependency (a database, a queue, a cache) can silently\nbreak every service that already had a connection open to it — even after the\ndependency itself reports healthy again. Restart dependents explicitly; don't assume\nthey'll notice on their own.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-we-found-and-what-it-cost-to-fix\">What we found, and what it cost to fix<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/#what-we-found-and-what-it-cost-to-fix\" class=\"hash-link\" aria-label=\"Direct link to What we found, and what it cost to fix\" title=\"Direct link to What we found, and what it cost to fix\" translate=\"no\">​</a></h2>\n<table><thead><tr><th>Container</th><th>Before</th><th>Fix</th><th>Broke anything?</th></tr></thead><tbody><tr><td><code>langfuse-postgres-1</code></td><td>root (<code>uid 0</code>)</td><td><code>user: \"999:999\"</code></td><td>No — recreated clean</td></tr><tr><td><code>langfuse-redis-1</code></td><td>root (<code>uid 0</code>)</td><td><code>user: \"999:999\"</code></td><td>No — recreated clean</td></tr><tr><td><code>langfuse-clickhouse-1</code></td><td>already <code>101:101</code></td><td>none needed</td><td>—</td></tr><tr><td><code>langfuse-langfuse-worker-1</code></td><td>untouched</td><td>none — just needed a restart</td><td>Yes — lost its Redis connection after the <em>other</em> two containers recreated</td></tr></tbody></table>\n<p>Two one-line fixes. The actual cost wasn't the fix — it was noticing the third\ncontainer that nobody touched was the one that broke.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting\">Troubleshooting<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/#troubleshooting\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting\" title=\"Direct link to Troubleshooting\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"docker-compose-restart-said-no-configuration-file-provided-not-found\"><code>docker compose restart</code> said \"no configuration file provided: not found\"<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/#docker-compose-restart-said-no-configuration-file-provided-not-found\" class=\"hash-link\" aria-label=\"Direct link to docker-compose-restart-said-no-configuration-file-provided-not-found\" title=\"Direct link to docker-compose-restart-said-no-configuration-file-provided-not-found\" translate=\"no\">​</a></h3>\n<p>You're not in the project directory Compose expects — it only finds <code>docker-compose.yml</code>\nrelative to your current working directory (or via <code>-f</code>). <code>cd</code> into the stack's own\nfolder first; this exact error means Compose never even looked at your containers.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-workers-still-erroring-after-i-restarted-redis\">The worker's still erroring after I restarted Redis<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/#the-workers-still-erroring-after-i-restarted-redis\" class=\"hash-link\" aria-label=\"Direct link to The worker's still erroring after I restarted Redis\" title=\"Direct link to The worker's still erroring after I restarted Redis\" translate=\"no\">​</a></h3>\n<p>Restarting the <em>dependency</em> doesn't help — the connection that broke belongs to the\n<em>dependent</em> service. Restart the thing holding the connection (here, <code>langfuse-worker</code>),\nnot the thing it connects to.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"docker-exec-container-id-username-returns-no-such-user\"><code>docker exec &lt;container&gt; id &lt;username&gt;</code> returns \"no such user\"<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/#docker-exec-container-id-username-returns-no-such-user\" class=\"hash-link\" aria-label=\"Direct link to docker-exec-container-id-username-returns-no-such-user\" title=\"Direct link to docker-exec-container-id-username-returns-no-such-user\" translate=\"no\">​</a></h3>\n<p>The image doesn't ship a dedicated non-root user under that name — check the image's own\ndocumentation, or <code>docker exec &lt;container&gt; cat /etc/passwd</code> to see what's actually\navailable before picking a UID.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Run containers as non-root</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>Privilege is the second layer: network hardening keeps a compromise from spreading\nbetween stacks, non-root keeps a compromise from owning the whole container. The last\nlayer in this series is identity — putting real authentication in front of the ports\nthat must stay exposed, which is where <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/\">NetBird</a>\nand Caddy come in.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.docker.com/reference/dockerfile/#user\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker docs — specify a user to run the container</a></li>\n<li class=\"\"><a href=\"https://hub.docker.com/_/postgres\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">PostgreSQL Docker image — running as a non-root user</a></li>\n<li class=\"\"><a href=\"https://github.com/redis/ioredis#connection-events\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">ioredis — connection and reconnection behavior</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/",
            "title": "Root by default: hardening container privilege on a self-hosted AI stack",
            "summary": "Two of the six containers behind a live Langfuse deployment were running as root, for no reason anyone chose. Fixing it took one line each — and broke a service that had nothing to do with the fix. A real conversion, a real coordination failure, and how to catch both.",
            "date_modified": "2026-08-10T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "docker",
                "security",
                "containers",
                "self-hosting",
                "agents",
                "hardening"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/",
            "content_html": "<div class=\"tutorialHero\"><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 10 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->10</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->10<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">1</span><span class=\"skillTracker__skill\" data-state=\"current\">Lock down Docker networks</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run containers as non-root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sso-langfuse-authentik-oidc/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/authentik-groups-gateway-access-control/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Membership, not just an account</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/oauth2-client-credentials-agent-gateway/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">An identity for the agent, not a key</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/mcp-server-jwt-auth/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">A tool server that checks who is asking</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/agent-egress-firewall-docker/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Where the agent can go, not just what it can call</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/rootless-docker-agent-host/\"><span class=\"skillTracker__dot\" data-state=\"locked\">8</span><span class=\"skillTracker__skill\" data-state=\"locked\">Move the daemon off root</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/sandbox-untrusted-agent-code/\"><span class=\"skillTracker__dot\" data-state=\"locked\">9</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run the model's own code without trusting it</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/per-tool-scopes-mcp-challenge/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">A scope per tool, and a refusal clients can act on</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>You start with one agent. Then it needs a database. Then you add a second agent, a\nmessaging bridge, an observability stack. Six months later a single WEC Instance is running\nfive compose projects, twenty-something containers, and nobody remembers which ports are\nopen to the world.</p>\n<p>That's not a hypothetical — that's the box this tutorial was written on. So instead of\ntheorizing, we probed it: can containers reach each other across stacks? Can they reach the\ndatabases? Is the firewall actually protecting anything?</p>\n<p>Three of the answers surprised me. One of them was a database sitting there with no\npassword. And the firewall — the firewall was lying.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-box-were-auditing\">The box we're auditing<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#the-box-were-auditing\" class=\"hash-link\" aria-label=\"Direct link to The box we're auditing\" title=\"Direct link to The box we're auditing\" translate=\"no\">​</a></h2>\n<p>Five independent compose projects, deployed over months, each with its own network (<code>host</code>, <code>none</code> and <code>bridge</code> are Docker's built-ins, plus one leftover from a service that isn't running):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> network </span><span class=\"token function\" style=\"color:#d73a49\">ls</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">NETWORK ID     NAME                             DRIVER    SCOPE</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">655850b24125   bridge                           bridge    local</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">05e5d49e7e9d   evolution-api_default            bridge    local</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">e8bd75ecbb0a   host                             host      local</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ce697baac173   langfuse_default                 bridge    local</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ead3e0822ce6   nlp-evaluation-service_nlp-net   bridge    local</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">5d0ab32d14d7   none                             null      local</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">9a6727150f2b   openclaw_default                 bridge    local</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ec77b98f457b   rag-service_default              bridge    local</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">5f88cabf97f0   remark42_default                 bridge    local</span><br></div></code></pre></div></div>\n<p>And here's what's published to the world:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--format</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{{.Names}}\\t{{.Ports}}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'0.0.0.0'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">remark42-remark42-1             0.0.0.0:8082-&gt;8080/tcp, [::]:8082-&gt;8080/tcp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">evolution-api-evolution-api-1   0.0.0.0:8080-&gt;8080/tcp, [::]:8080-&gt;8080/tcp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langfuse-langfuse-web-1         0.0.0.0:3001-&gt;3000/tcp, [::]:3001-&gt;3000/tcp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">langfuse-minio-1                0.0.0.0:9090-&gt;9000/tcp, [::]:9090-&gt;9000/tcp, 127.0.0.1:9091-&gt;9001/tcp</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The exposure audit — nine networks listed, four services bound to every interface\" src=\"https://development-wec.wiline.com/docs/assets/images/netsec-exposure-audit-904ef03d2c7552d9b1b8c9d9e69218dd.png\" width=\"1045\" height=\"300\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> The start of any audit. Nine networks: three Docker built-ins (<code>bridge</code>, <code>host</code>,\n<code>none</code>), five compose projects, and one leftover. Four services published on <code>0.0.0.0</code> — every\ninterface, including the public one.</p>\n<p>Four services listening on every interface. Note what's <em>not</em> in that list: the Postgres,\nClickHouse and Redis behind Langfuse. Those were published to <code>127.0.0.1</code> instead — someone\nmade a good decision there. Hold that thought.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"test-1--do-separate-docker-networks-actually-isolate-yes-and-most-blogs-are-wrong\">Test 1 — Do separate Docker networks actually isolate? (yes, and most blogs are wrong)<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#test-1--do-separate-docker-networks-actually-isolate-yes-and-most-blogs-are-wrong\" class=\"hash-link\" aria-label=\"Direct link to Test 1 — Do separate Docker networks actually isolate? (yes, and most blogs are wrong)\" title=\"Direct link to Test 1 — Do separate Docker networks actually isolate? (yes, and most blogs are wrong)\" translate=\"no\">​</a></h2>\n<p>The classic worry: OpenClaw gets compromised, and the attacker walks into Langfuse's\ndatabase next door. Let's find out instead of guessing. Grab the container IPs:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">c</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> langfuse-postgres-1 evolution-api-evolution-postgres-1</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> inspect </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-f</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{{.Name}} {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$c</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">done</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">/langfuse-postgres-1 172.19.0.5</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">/evolution-api-evolution-postgres-1 172.23.0.4</span><br></div></code></pre></div></div>\n<p>Now try to reach both <strong>from inside the OpenClaw container</strong>, by IP — no DNS, no shortcuts:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> openclaw-openclaw-gateway-1 </span><span class=\"token function\" style=\"color:#d73a49\">node</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">const net=require(\"net\");</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">[[\"langfuse-postgres\",\"172.19.0.5\",5432],[\"evolution-postgres\",\"172.23.0.4\",5432]]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  .forEach(([n,ip,p])=&gt;{</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    const s=net.connect({host:ip,port:p,timeout:4000});</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    s.on(\"connect\",()=&gt;{console.log(`REACHED   ${n}`);s.destroy()});</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    s.on(\"timeout\",()=&gt;{console.log(`blocked   ${n}`);s.destroy()});</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    s.on(\"error\",e=&gt;console.log(`error     ${n} ${e.code}`));</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  });'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocked   langfuse-postgres</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocked   evolution-postgres</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Cross-network probes timing out from inside the OpenClaw container\" src=\"https://development-wec.wiline.com/docs/assets/images/netsec-isolation-blocked-52d0644c0dbd63427ca62b09eb5b9947.png\" width=\"759\" height=\"182\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> Connecting by raw IP from one stack's container to another stack's database:\nboth time out. Docker's isolation rules are doing their job.</p>\n<p><strong>Blocked.</strong> Modern Docker installs <code>DOCKER-ISOLATION-STAGE</code> rules that drop traffic between\nuser-defined bridge networks, and they work. If you've read that \"Docker networks don't\nreally isolate,\" test it on your own box before believing it — on current Docker they do.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can verify network isolation empirically instead of trusting folklore: get the target's\nIP with <code>docker inspect</code>, then connect <strong>by IP</strong> from inside another container.</p></div></div>\n<p>So cross-stack isolation is fine. Which means the real risk is somewhere else.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"test-2--inside-a-network-everything-is-wide-open\">Test 2 — Inside a network, everything is wide open<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#test-2--inside-a-network-everything-is-wide-open\" class=\"hash-link\" aria-label=\"Direct link to Test 2 — Inside a network, everything is wide open\" title=\"Direct link to Test 2 — Inside a network, everything is wide open\" translate=\"no\">​</a></h2>\n<p>Each compose project puts all its services on one network. That's the default, and it means\nthe application container can reach every backing service — which is <em>intended</em>. The\nquestion is what that access actually looks like. From Evolution API's bridge container:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> evolution-api-bridge-1 python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">import socket</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for name, host, port in [(\"postgres\",\"evolution-postgres\",5432),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">                         (\"redis\",\"evolution-redis\",6379),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">                         (\"api\",\"evolution-api\",8080)]:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    s=socket.socket(); s.settimeout(4)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    try:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        s.connect((host,port)); print(f\"REACHED   {name}:{port}\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    except Exception as e:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        print(f\"blocked   {name}:{port} -- {type(e).__name__}\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    finally: s.close()'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">REACHED   postgres:5432</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">REACHED   redis:6379</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">REACHED   api:8080</span><br></div></code></pre></div></div>\n<p>Reachable is expected. <strong>Unauthenticated is not.</strong> Check whether those services actually ask\nfor credentials — first Redis:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> evolution-api-bridge-1 python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">import socket</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">s=socket.socket(); s.settimeout(5); s.connect((\"evolution-redis\",6379))</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">s.sendall(b\"INFO server\\r\\n\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print(s.recv(200).decode(errors=\"replace\")[:120])'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">$625</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># Server</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">redis_version:7.4.9</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">redis_git_sha1:00000000</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">redis_git_dirty:0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">redis_build_id:b61b4eb609520881</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">redis_</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Redis answering INFO with no AUTH\" src=\"https://development-wec.wiline.com/docs/assets/images/netsec-redis-noauth-22e25e2b5bbf007d514a914ec9ec8b91.png\" width=\"744\" height=\"226\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> The finding: <code>INFO server</code> answered with no credentials (output truncated at 120\nchars by the probe). Any container on this network can read or wipe the session store.</p>\n<p>That's a <strong><code>INFO</code> command answered with no <code>AUTH</code></strong> — the Redis holding this deployment's\nsession state has no password. Anything that gets code execution in <em>any</em> container on that\nnetwork can read it, write to it, or <code>FLUSHALL</code> it.</p>\n<p>Postgres, in the same stack, behaves correctly:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> evolution-api-bridge-1 python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">import socket,struct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">s=socket.socket(); s.settimeout(5); s.connect((\"evolution-postgres\",5432))</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">msg=b\"user\\x00postgres\\x00database\\x00postgres\\x00\\x00\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">s.sendall(struct.pack(\"!ii\",len(msg)+8,196608)+msg)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print(s.recv(64))'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">b'R\\x00\\x00\\x00\\x17\\x00\\x00\\x00\\nSCRAM-SHA-256\\x00\\x00'</span><br></div></code></pre></div></div>\n<p>It demands SCRAM-SHA-256. Same box, same network, two backing services — one asks for a\npassword, the other doesn't. Nobody decided that; it's just what the images default to when\nyou don't set a password.</p>\n<p><strong>The lesson: \"internal network\" is not a security boundary.</strong> It's a convenience. Treat\nevery service as if the attacker is already on that network, because if one container falls,\nthey are.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"test-3--the-firewall-that-isnt\">Test 3 — The firewall that isn't<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#test-3--the-firewall-that-isnt\" class=\"hash-link\" aria-label=\"Direct link to Test 3 — The firewall that isn't\" title=\"Direct link to Test 3 — The firewall that isn't\" translate=\"no\">​</a></h2>\n<p>Now the big one. Turn on UFW and deny everything inbound except SSH:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> ufw allow </span><span class=\"token number\" style=\"color:#36acaa\">22</span><span class=\"token plain\">/tcp          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># ALWAYS first, or you lock yourself out</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> ufw default deny incoming</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> ufw </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--force</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">enable</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> ufw status verbose</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Status: active</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Logging: on (low)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Default: deny (incoming), allow (outgoing), deny (routed)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">New profiles: skip</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">To                         Action      From</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">--                         ------      ----</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">60000:61000/udp            ALLOW IN    Anywhere</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">22/tcp                     ALLOW IN    Anywhere</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">60000:61000/udp (v6)       ALLOW IN    Anywhere (v6)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">22/tcp (v6)                ALLOW IN    Anywhere (v6)</span><br></div></code></pre></div></div>\n<p>Deny incoming — only SSH and mosh's UDP range allowed. Now, from a <strong>different machine</strong>,\nopen the Langfuse UI and the Evolution API:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">From a laptop, not the server</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">http://&lt;instance-ip&gt;:3001   -&gt;  Langfuse UI loads normally</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">http://&lt;instance-ip&gt;:8080   -&gt;  {\"status\":200,\"message\":\"Welcome to the Evolution API...\"}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Evolution API answering in a browser while UFW denies all incoming traffic\" src=\"https://development-wec.wiline.com/docs/assets/images/netsec-ufw-bypass-130569873d3680226c3534f016c4756a.png\" width=\"474\" height=\"239\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 4.</strong> The firewall says it denies everything inbound. The Evolution API on <code>:8080</code>\nanswers anyway — status 200, straight from a browser on another machine.</p>\n<p>Both wide open, through a firewall configured to block them. This is the single most common\nsecurity surprise in self-hosted Docker, and it isn't a bug.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"why-docker-gets-to-the-packet-first\">Why: Docker gets to the packet first<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#why-docker-gets-to-the-packet-first\" class=\"hash-link\" aria-label=\"Direct link to Why: Docker gets to the packet first\" title=\"Direct link to Why: Docker gets to the packet first\" translate=\"no\">​</a></h3>\n<p>Look at the order of the <code>FORWARD</code> chain:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-L</span><span class=\"token plain\"> FORWARD </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-n</span><span class=\"token plain\"> --line-numbers </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">head</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-8</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Chain FORWARD (policy DROP)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">num  target     prot opt source               destination</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">1    DOCKER-USER  all  --  0.0.0.0/0            0.0.0.0/0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">2    DOCKER-FORWARD  all  --  0.0.0.0/0            0.0.0.0/0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">3    ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">4    ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">5    ufw-before-logging-forward  all  --  0.0.0.0/0            0.0.0.0/0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">6    ufw-before-forward  all  --  0.0.0.0/0            0.0.0.0/0</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"UFW active with deny incoming, and Docker&amp;#39;s chains ahead of UFW&amp;#39;s in FORWARD\" src=\"https://development-wec.wiline.com/docs/assets/images/netsec-ufw-chains-7a5553d8ce1882eb415c7e12e1beb065.png\" width=\"1031\" height=\"395\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 5.</strong> The whole explanation in one screen: the firewall says <em>deny incoming</em>, and\nDocker's chains sit at positions 1-2 while UFW's start at 5.</p>\n<p>Docker's chains sit at positions <strong>1 and 2</strong>. UFW's don't appear until <strong>5 and 6</strong>. And\npublishing a port isn't a listener on the host — it's a NAT rule:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-t</span><span class=\"token plain\"> nat </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-L</span><span class=\"token plain\"> DOCKER </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-n</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-E</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'3001|8080'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">DNAT tcp -- 0.0.0.0/0  0.0.0.0/0  tcp dpt:8080 to:172.23.0.3:8080</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">DNAT tcp -- 0.0.0.0/0  0.0.0.0/0  tcp dpt:3001 to:172.19.0.2:3000</span><br></div></code></pre></div></div>\n<p>The packet arrives, Docker's NAT rewrites the destination to the container, and it becomes\n<em>forwarded</em> traffic that Docker's own chain accepts — long before any UFW rule is consulted.\nYour UFW rules aren't ignored; they're simply never reached.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>This is the one to remember</div><div class=\"admonitionContent_BuS1\"><p><code>ufw deny</code> does <strong>not</strong> protect a published Docker port. If you've been relying on UFW in\nfront of <code>-p 8080:8080</code>, that port has been open the whole time.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"four-fixes-in-order-of-preference\">Four fixes, in order of preference<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#four-fixes-in-order-of-preference\" class=\"hash-link\" aria-label=\"Direct link to Four fixes, in order of preference\" title=\"Direct link to Four fixes, in order of preference\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"fix-1--dont-publish-it-at-all\">Fix 1 — Don't publish it at all<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#fix-1--dont-publish-it-at-all\" class=\"hash-link\" aria-label=\"Direct link to Fix 1 — Don't publish it at all\" title=\"Direct link to Fix 1 — Don't publish it at all\" translate=\"no\">​</a></h3>\n<p>The cleanest fix isn't a firewall rule; it's not opening the door. If a service is only\nconsumed by other containers, it needs <strong>no</strong> <code>ports:</code> entry — service-to-service traffic\nworks over the compose network by name.</p>\n<p>If you need it reachable from the host (a debugging port, a local psql), bind it to\nloopback explicitly:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">services</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">postgres</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 127.0.0.1 prefix = host-only, never the public interface</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">ports</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"127.0.0.1:5433:5432\"</span><br></div></code></pre></div></div>\n<p>That's exactly what the Langfuse stack on this box already does — its Postgres, ClickHouse\nand Redis are all on <code>127.0.0.1</code>, which is why they never showed up in our exposure audit.\nThe default <code>\"5433:5432\"</code> means <code>0.0.0.0</code> — every interface, including the public one.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"fix-2--split-networks-by-trust-zone-and-mark-backends-internal\">Fix 2 — Split networks by trust zone, and mark backends <code>internal</code><a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#fix-2--split-networks-by-trust-zone-and-mark-backends-internal\" class=\"hash-link\" aria-label=\"Direct link to fix-2--split-networks-by-trust-zone-and-mark-backends-internal\" title=\"Direct link to fix-2--split-networks-by-trust-zone-and-mark-backends-internal\" translate=\"no\">​</a></h3>\n<p>One network per compose project is the default, not a design. Give the backing services\ntheir own network, and put only the application in both:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.yml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">services</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">app</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">                      </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># talks to the world AND to the database</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> your</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">agent</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">networks</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">frontend</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> backend</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">db</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> redis</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">7</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">command</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> redis</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">server </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">requirepass $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">REDIS_PASSWORD</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">networks</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">backend</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\">     </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># backend only — no route out</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">public</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">                   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># e.g. a webhook receiver</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> your</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">public</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">thing</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">networks</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">frontend</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># cannot see the database at all</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token key atrule\" style=\"color:#00a4db\">networks</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">frontend</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">backend</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">internal</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:#36acaa\">true</span><span class=\"token plain\">          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># no gateway: no inbound, no outbound</span><br></div></code></pre></div></div>\n<p>Verified on the same box:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># the frontend-only container tries to reach the database</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> netsecdemo-public-1 python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">import socket</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">s=socket.socket(); s.settimeout(4)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">try: s.connect((\"db\",6379)); print(\"REACHED db  &lt;- leak\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">except Exception as e: print(f\"blocked: {type(e).__name__}\")'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># the app, which is on both networks</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> netsecdemo-app-1 python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">import socket</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">s=socket.socket(); s.settimeout(4)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">try: s.connect((\"db\",6379)); print(\"REACHED db  &lt;- correct, it needs it\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">except Exception as e: print(f\"blocked: {type(e).__name__}\")'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># and can the database itself reach the internet?</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> netsecdemo-db-1 </span><span class=\"token function\" style=\"color:#d73a49\">timeout</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">6</span><span class=\"token plain\"> getent hosts pypi.org </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">/dev/null </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"resolved external DNS\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">||</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"no internet egress  &lt;- internal working\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">blocked: gaierror</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">REACHED db  &lt;- correct, it needs it</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">no internet egress  &lt;- internal working</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The internal-network demo: public blocked, app allowed, database with no egress\" src=\"https://development-wec.wiline.com/docs/assets/images/netsec-internal-demo-6231d6936686cf9d282c7f494797a185.png\" width=\"371\" height=\"163\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 6.</strong> Three properties proven in one run — the public-facing container can't even\nresolve the database, the app can reach it, and the database has no route to the internet.</p>\n<p>Three properties at once: the public-facing container can't even resolve the database, the\napp can, and the database can't phone home — which matters the day a dependency of yours\nturns malicious.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"fix-3--authenticate-everything-including-internal-services\">Fix 3 — Authenticate everything, including \"internal\" services<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#fix-3--authenticate-everything-including-internal-services\" class=\"hash-link\" aria-label=\"Direct link to Fix 3 — Authenticate everything, including &quot;internal&quot; services\" title=\"Direct link to Fix 3 — Authenticate everything, including &quot;internal&quot; services\" translate=\"no\">​</a></h3>\n<p>Our audit found Redis answering <code>INFO</code> with no credentials. One line fixes it:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">db</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> redis</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">7</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">command</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> redis</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">server </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">requirepass $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">REDIS_PASSWORD</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (after)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-NOAUTH Authentication required.</span><br></div></code></pre></div></div>\n<p>That's the real output from this box, after applying the fix — the finding from Test 2 is\nclosed. If you set a password on a Redis that something already uses, remember to update the\nclient's connection string too (<code>redis://:&lt;password&gt;@host:6379/0</code>), or you'll trade an open\ndatabase for a broken one.</p>\n<p>Do this even for services that aren't published. Defense in depth means the second layer\nholds when the first one fails.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"fix-4--when-you-must-publish-filter-in-docker-user\">Fix 4 — When you must publish, filter in <code>DOCKER-USER</code><a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#fix-4--when-you-must-publish-filter-in-docker-user\" class=\"hash-link\" aria-label=\"Direct link to fix-4--when-you-must-publish-filter-in-docker-user\" title=\"Direct link to fix-4--when-you-must-publish-filter-in-docker-user\" translate=\"no\">​</a></h3>\n<p>Sometimes a port genuinely has to be public-facing but restricted by source. The chain to\nuse is <code>DOCKER-USER</code> — Docker guarantees it runs <strong>before</strong> its own accept rules, and never\noverwrites it:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># only 203.0.113.0/24 may open new connections to the container's port 3000</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-I</span><span class=\"token plain\"> DOCKER-</span><span class=\"token environment constant\" style=\"color:#36acaa\">USER</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> tcp </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--dport</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3000</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> conntrack </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--ctstate</span><span class=\"token plain\"> NEW </span><span class=\"token operator\" style=\"color:#393A34\">!</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">203.0</span><span class=\"token plain\">.113.0/24 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-j</span><span class=\"token plain\"> DROP</span><br></div></code></pre></div></div>\n<p>Tested live on the Langfuse port, from a browser on another machine:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Before the rule</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">http://&lt;instance-ip&gt;:3001  -&gt;  Langfuse UI loads</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">After the rule</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ERR_CONNECTION_TIMED_OUT</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The same URL timing out after the DOCKER-USER rule\" src=\"https://development-wec.wiline.com/docs/assets/images/netsec-docker-user-blocked-8e34c54fd3b4552806e49bf87aed7cfb.png\" width=\"2524\" height=\"1288\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><strong>Figure 7.</strong> Same URL, same firewall, one rule in the right chain — and the port is finally\nclosed. Note Chrome's own suggestion: <em>\"Checking the proxy and the firewall.\"</em> This time the\nfirewall really is the answer.</p>\n<p>Meanwhile the Evolution API on <code>:8080</code>, which we deliberately left alone, kept answering.\nThe rule is surgical, and it works exactly where UFW couldn't.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Make it survive a reboot</div><div class=\"admonitionContent_BuS1\"><p><code>iptables -I</code> is not persistent. Use <code>iptables-persistent</code> (<code>netfilter-persistent save</code>), or\nput the rule in a small systemd unit that runs after <code>docker.service</code>. A firewall rule that\ndisappears on reboot is worse than none, because you'll think you're protected.</p></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can audit and lock down a multi-stack Docker host: prove what's reachable, bind or split\nwhat shouldn't be, authenticate the backends, and filter published ports in the one chain\nDocker respects.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-audit-as-a-checklist\">The audit, as a checklist<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#the-audit-as-a-checklist\" class=\"hash-link\" aria-label=\"Direct link to The audit, as a checklist\" title=\"Direct link to The audit, as a checklist\" translate=\"no\">​</a></h2>\n<p>Run these four commands on any box you own. They take a minute and tell you more than any\narchitecture diagram:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 1. What's exposed to the world?</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--format</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{{.Names}}\\t{{.Ports}}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'0.0.0.0'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 2. Are your firewall rules even in the path? (Docker chains before ufw = they aren't)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> iptables </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-L</span><span class=\"token plain\"> FORWARD </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-n</span><span class=\"token plain\"> --line-numbers </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">head</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-8</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 3. What can a compromised container reach on its own network?</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">container</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"import socket;s=socket.socket();s.settimeout(3);s.connect</span><span class=\"token string variable punctuation\" style=\"color:#393A34\">((</span><span class=\"token string variable\" style=\"color:#36acaa\">'</span><span class=\"token string variable operator\" style=\"color:#393A34\">&lt;</span><span class=\"token string variable\" style=\"color:#36acaa\">svc</span><span class=\"token string variable operator\" style=\"color:#393A34\">&gt;</span><span class=\"token string variable\" style=\"color:#36acaa\">'</span><span class=\"token string variable punctuation\" style=\"color:#393A34\">,</span><span class=\"token string variable operator\" style=\"color:#393A34\">&lt;</span><span class=\"token string variable\" style=\"color:#36acaa\">port</span><span class=\"token string variable operator\" style=\"color:#393A34\">&gt;</span><span class=\"token string variable punctuation\" style=\"color:#393A34\">))</span><span class=\"token string\" style=\"color:#e3116c\">;print('reached')\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 4. Does that backing service ask for a password?</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">container</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">import socket;s=socket.socket();s.settimeout(4);s.connect</span><span class=\"token string variable punctuation\" style=\"color:#393A34\">((</span><span class=\"token string variable\" style=\"color:#36acaa\">'redis'</span><span class=\"token string variable punctuation\" style=\"color:#393A34\">,</span><span class=\"token string variable number\" style=\"color:#36acaa\">6379</span><span class=\"token string variable punctuation\" style=\"color:#393A34\">))</span><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">s.sendall(b'INFO server</span><span class=\"token string entity\" style=\"color:#36acaa\">\\r</span><span class=\"token string entity\" style=\"color:#36acaa\">\\n</span><span class=\"token string\" style=\"color:#e3116c\">');print(s.recv(60))\"</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-we-found-and-what-it-cost-to-fix\">What we found, and what it cost to fix<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#what-we-found-and-what-it-cost-to-fix\" class=\"hash-link\" aria-label=\"Direct link to What we found, and what it cost to fix\" title=\"Direct link to What we found, and what it cost to fix\" translate=\"no\">​</a></h2>\n<table><thead><tr><th>Finding</th><th>Severity</th><th>Fix</th></tr></thead><tbody><tr><td>Cross-network isolation works</td><td>✅ none</td><td>nothing — Docker already does this</td></tr><tr><td>Every service on one network, mutually reachable</td><td>⚠️ medium</td><td>split by trust zone, <code>internal: true</code></td></tr><tr><td>Redis answering with no <code>AUTH</code></td><td>🔴 high</td><td>one line: <code>--requirepass</code></td></tr><tr><td><code>ufw deny</code> not applied to published ports</td><td>🔴 high</td><td>bind to <code>127.0.0.1</code>, or <code>DOCKER-USER</code> rule</td></tr><tr><td>Backing DBs on <code>127.0.0.1</code> (Langfuse)</td><td>✅ none</td><td>already correct — copy this pattern</td></tr></tbody></table>\n<p>None of the fixes took more than a line of YAML. The expensive part was <em>knowing which line</em>\n— and the only way to know is to probe your own box instead of trusting the diagram in your\nhead.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting\">Troubleshooting<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#troubleshooting\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting\" title=\"Direct link to Troubleshooting\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"i-added-a-docker-user-rule-and-nothing-changed\">I added a <code>DOCKER-USER</code> rule and nothing changed<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#i-added-a-docker-user-rule-and-nothing-changed\" class=\"hash-link\" aria-label=\"Direct link to i-added-a-docker-user-rule-and-nothing-changed\" title=\"Direct link to i-added-a-docker-user-rule-and-nothing-changed\" translate=\"no\">​</a></h3>\n<p>Two usual causes. First, <code>--dport</code> must be the <strong>container's</strong> port, not the published one —\nthe DNAT already rewrote the destination by the time <code>DOCKER-USER</code> sees the packet (in our\ncase <code>3000</code>, not <code>3001</code>). Second, existing connections aren't affected: the\n<code>--ctstate NEW</code> match only touches new ones, so an already-open browser tab keeps working.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"i-enabled-ufw-and-lost-my-ssh-session\">I enabled UFW and lost my SSH session<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#i-enabled-ufw-and-lost-my-ssh-session\" class=\"hash-link\" aria-label=\"Direct link to I enabled UFW and lost my SSH session\" title=\"Direct link to I enabled UFW and lost my SSH session\" translate=\"no\">​</a></h3>\n<p><code>sudo ufw allow 22/tcp</code> <strong>before</strong> <code>ufw enable</code>, always. If you're already locked out, most\nproviders give you a serial/VNC console — use it to run <code>ufw disable</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"internal-true-broke-my-containers-package-install\"><code>internal: true</code> broke my container's package install<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#internal-true-broke-my-containers-package-install\" class=\"hash-link\" aria-label=\"Direct link to internal-true-broke-my-containers-package-install\" title=\"Direct link to internal-true-broke-my-containers-package-install\" translate=\"no\">​</a></h3>\n<p>That's it working. An internal network has no gateway, so no egress at all — no <code>pip install</code>, no DNS, no calling an external API. Backing services shouldn't need any of that;\nif a container does, it belongs on the frontend network too.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"services-cant-find-each-other-after-i-split-the-networks\">Services can't find each other after I split the networks<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#services-cant-find-each-other-after-i-split-the-networks\" class=\"hash-link\" aria-label=\"Direct link to Services can't find each other after I split the networks\" title=\"Direct link to Services can't find each other after I split the networks\" translate=\"no\">​</a></h3>\n<p>Compose DNS only resolves names <strong>within a shared network</strong>. If <code>app</code> and <code>db</code> are on\ndifferent networks with nothing in common, <code>db</code> won't resolve. The app has to be a member of\nboth, as in Fix 2.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Lock down Docker networks</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>Network hardening is the layer that keeps a compromised container from becoming a\ncompromised host. The next layers, in the order I'd do them: <strong>run containers as non-root</strong>\n(<code>user:</code> in compose) so a breakout starts with fewer privileges, and put an <strong>identity\nprovider in front of the ports you must expose</strong> — a private mesh with\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/\">NetBird</a> plus authentication is the natural pairing\nwith everything above.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/network/packet-filtering-firewalls/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker docs — packet filtering and firewalls</a></li>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/network/drivers/bridge/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker docs — bridge network driver options</a></li>\n<li class=\"\"><a href=\"https://redis.io/docs/latest/operate/oss_and_stack/management/security/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Redis security — authentication</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/harden-docker-networks-multi-agent/",
            "title": "Your firewall is lying to you: hardening Docker networks for multi-agent systems",
            "summary": "A box running five agent stacks, a firewall set to deny everything, and services still answering from the public internet. We probe a real deployment, find a database with no password, prove why UFW never sees Docker traffic, and fix it four ways — every command and result from a live run.",
            "date_modified": "2026-08-03T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "docker",
                "security",
                "networking",
                "self-hosting",
                "agents",
                "hardening"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/telegram-on-hermes/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/hermesagent-c76bbe81610ae2e70b0950e0f96bec8a.png\" alt=\"Hermes Agent\"><span class=\"tutorialHero__plus\">+</span><svg viewBox=\"0 0 24 24\" fill=\"#26A5E4\" aria-label=\"Telegram\" class=\"tutorialHero__docker\"><path d=\"M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 2 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->2</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->2<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting Hermes</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Agent with persistent memory</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">🏆</span><span class=\"skillTracker__skill\" data-state=\"current\">Telegram on Hermes</span></span></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>In <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/\">Part 1</a> you self-hosted Hermes with\npersistent memory. This part connects it to the <strong>same Telegram bot</strong> from the\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/\">OpenClaw series</a> — the chat your\nusers already know keeps working exactly as it did, just a different agent\nanswering underneath. No new bot to announce, no channel to migrate people to.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>Continues on the same WEC Instance as Part 1. Hermes gateway is already\ninstalled and running as a systemd user service — this part is configuration,\nnot installation. Every command and error below is from the actual run.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"why-keep-the-same-bot\">Why keep the same bot<a href=\"https://development-wec.wiline.com/docs/tutorials/telegram-on-hermes/#why-keep-the-same-bot\" class=\"hash-link\" aria-label=\"Direct link to Why keep the same bot\" title=\"Direct link to Why keep the same bot\" translate=\"no\">​</a></h2>\n<p>Self-hosting is only worth the effort if it doesn't lock you into whatever\nagent you started with. The bot itself — its name, its chat history, the\npeople who already talk to it — is the asset. The agent behind it is\nreplaceable. Swapping OpenClaw for Hermes on the same bot proves that: same\nTelegram front door, same conversation thread, new brain.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/telegram-on-hermes/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">Hermes running from <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/\">Part 1</a>, gateway\ninstalled as a systemd service.</li>\n<li class=\"\">A Telegram bot token. Reuse the one from the\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/\">OpenClaw Telegram tutorial</a> via\n<strong>@BotFather → /mybots → your bot → API Token</strong> — or create a fresh one with\n<strong>/newbot</strong> if you don't have the old one handy. Either way, the steps below\nare identical.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--run-the-setup-wizard\">Step 1 — Run the setup wizard<a href=\"https://development-wec.wiline.com/docs/tutorials/telegram-on-hermes/#step-1--run-the-setup-wizard\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Run the setup wizard\" title=\"Direct link to Step 1 — Run the setup wizard\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">hermes gateway setup</span><br></div></code></pre></div></div>\n<p>Pick Telegram from the platform list, then choose how to connect it:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">◆ Telegram</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  How would you like to create your Telegram bot?</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    [1] Automatic (recommended)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        Scan a QR code → confirm in Telegram → done.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        No token copy-paste needed.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    [2] Manual</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        Create a bot via @BotFather yourself and paste the token.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Choice [1/2] [1]: 2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  Create a bot via @BotFather on Telegram</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Telegram bot token: **********************************************</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">✓ Telegram token saved</span><br></div></code></pre></div></div>\n<p>Take option <strong>2</strong> — option 1 spins up a brand new bot via QR code, which\ndefeats the point if you're reusing an existing one.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--open-access-and-a-real-gotcha\">Step 2 — Open access (and a real gotcha)<a href=\"https://development-wec.wiline.com/docs/tutorials/telegram-on-hermes/#step-2--open-access-and-a-real-gotcha\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Open access (and a real gotcha)\" title=\"Direct link to Step 2 — Open access (and a real gotcha)\" translate=\"no\">​</a></h2>\n<p>Next the wizard asks who's allowed to use the bot:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">🔒 Security: Restrict who can use your bot</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">   To find your Telegram user ID:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">   1. Message @userinfobot on Telegram</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">   2. It will reply with your numeric ID (e.g., 123456789)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Allowed user IDs (comma-separated, leave empty for open access):</span><br></div></code></pre></div></div>\n<p>If you don't have your ID handy, leave it blank and move on — you can lock\nthis down later. But <strong>the prompt is misleading</strong>: leaving it empty doesn't\nopen access by default. The gateway logs told the real story after restart:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING gateway.run: No user allowlists configured. All unauthorized users</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">will be denied. Set GATEWAY_ALLOW_ALL_USERS=true in ~/.hermes/.env to allow</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">open access, or configure platform allowlists (e.g.,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">TELEGRAM_ALLOWED_USERS=your_id).</span><br></div></code></pre></div></div>\n<p>An empty allowlist means <strong>deny everyone</strong>, not open access. If you want open\naccess (fine for a personal bot, not for anything public-facing), you need\none more step:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'GATEWAY_ALLOW_ALL_USERS=true'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;&gt;</span><span class=\"token plain\"> ~/.hermes/.env</span><br></div></code></pre></div></div>\n<p>The wizard also asks for a <strong>Home Channel</strong> — where Hermes delivers cron\nresults and cross-platform messages. Leave it blank too; you can set it later\nfrom inside the chat with <code>/sethome</code>.</p>\n<p>Finish the wizard and let it restart the gateway:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Restart the gateway to pick up changes? [Y/n]: Y</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">✓ User service restarted</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal showing the full wizard run — token saved, the allowlist warning, and the gateway restart\" src=\"https://development-wec.wiline.com/docs/assets/images/hermes-gateway-setup-wizard-7293ce9021ee147b93bad2dcb7a72a53.png\" width=\"603\" height=\"949\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> The whole wizard, start to finish — token, the allowlist warning, and the restart.</p>\n<p>If you added <code>GATEWAY_ALLOW_ALL_USERS</code> after the wizard already restarted\nonce (as above), restart again so it picks up the <code>.env</code> change:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">systemctl </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--user</span><span class=\"token plain\"> restart hermes-gateway.service</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--verify\">Step 3 — Verify<a href=\"https://development-wec.wiline.com/docs/tutorials/telegram-on-hermes/#step-3--verify\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — Verify\" title=\"Direct link to Step 3 — Verify\" translate=\"no\">​</a></h2>\n<p>Message your bot. <strong>Not @BotFather</strong> — your bot, by the username you gave it\nwhen you created it. It's an easy mix-up: BotFather answers everything with\nits own fixed command menu, so if you see that instead of a real reply,\nyou're in the wrong chat.</p>\n<p>The first message may get a system nudge instead of a chat reply — that's\nHermes noticing there's no home channel yet, not a failure:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">📬 No home channel is set for Telegram. A home channel is where Hermes</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">delivers cron job results and cross-platform messages.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Type /sethome to make this chat your home channel, or ignore to skip.</span><br></div></code></pre></div></div>\n<p>Send another message and it answers for real:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Telegram chat showing the bot replying &amp;quot;Hello! How can I help?&amp;quot; via Hermes\" src=\"https://development-wec.wiline.com/docs/assets/images/hermes-telegram-verified-17c22677d5e1e6a1d6147099b58fdc53.png\" width=\"1320\" height=\"2868\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> The same bot from the OpenClaw series, now answering through Hermes.</p>\n<p>Ask it something with actual depth, and it delivers a real, structured answer\n— not just a canned reply:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Telegram chat showing the bot explaining Rayleigh scattering in response to &amp;quot;Why is the sky blue?&amp;quot;\" src=\"https://development-wec.wiline.com/docs/assets/images/hermes-telegram-sky-blue-139494c527c4075440bf7afa25c14039.png\" width=\"1320\" height=\"2868\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> Same agent, a real question — full explanation, not a one-liner.</p>\n<p>Same bot, same chat history, new agent.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting-real\">Troubleshooting (real)<a href=\"https://development-wec.wiline.com/docs/tutorials/telegram-on-hermes/#troubleshooting-real\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting (real)\" title=\"Direct link to Troubleshooting (real)\" translate=\"no\">​</a></h2>\n<p><strong>\"No user allowlists configured. All unauthorized users will be denied.\"</strong> —\nleaving the allowlist prompt empty does not mean open access; it means\ndeny-all. Set <code>GATEWAY_ALLOW_ALL_USERS=true</code> in <code>~/.hermes/.env</code> and restart\nthe gateway.</p>\n<p><strong>\"Telegram polling conflict... make sure that only one bot instance is\nrunning\"</strong> — shows up for a few seconds right after a restart, while\nTelegram's servers finish releasing the previous session. It retries and\nclears on its own within 20 seconds; no action needed unless it keeps\nrepeating past the 5th retry, which means another process really is polling\nthe same token.</p>\n<p><strong>Bot doesn't respond at all</strong> — check you're messaging the bot itself, not\n@BotFather. BotFather always answers with its own command list regardless of\nwhat you send it.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/telegram-on-hermes/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>The bot answers — next is putting it on a schedule: a morning briefing or a\nservice-health check delivered to this same chat, no polling required.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Telegram on Hermes</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>",
            "url": "https://development-wec.wiline.com/docs/tutorials/telegram-on-hermes/",
            "title": "Migrate OpenClaw's Telegram Bot to Hermes",
            "summary": "Move the same Telegram bot from the OpenClaw series onto Hermes — same chat, same users, a different agent answering underneath. Real setup wizard, a real allowlist gotcha, and proof it answers.",
            "date_modified": "2026-07-29T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "self-hosting",
                "hermes",
                "telegram",
                "messaging"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/web-search-wiline-inference/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/searxng-mark-1b536bb949960590cea598d56becda08.png\" alt=\"SearXNG\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 8 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->8</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->8<!-- --> earned</span></div><div class=\"skillTracker__series\">AI evals &amp; observability</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove a model works</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trustworthy JSON</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real test data at scale</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Observe &amp; score production</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">RAG, end to end</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">Catch regressions in CI</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trace &amp; debug agent tool calls</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">🏆</span><span class=\"skillTracker__skill\" data-state=\"current\">Add live web search</span></span></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Say you've built a support chatbot or a RAG assistant on WEC. It's solid on your docs and\non what the model already knows — but ask it something <em>current</em> (\"what's the latest\nrelease of X?\", today's pricing, a recent change) and it either guesses or tells you its\nknowledge is out of date. For anything that needs to stay current, that's a real gap.</p>\n<p>WEC Inference now has a <strong>web-search tool</strong> built in. Add it to a normal\n<code>/v1/chat/completions</code> call and the model can look things up on the live web — the search\nruns on WiLine's own infrastructure (SearXNG), so nothing goes out to a third-party search\nvendor. Let's test it.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>Calls hit <code>https://inference.wiline.com/v1/chat/completions</code> with your WEC Inference API\nkey, model <code>Qwen3.5-9B</code>. Nothing to install — the tool is part of the API.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"without-web-search\">Without web search<a href=\"https://development-wec.wiline.com/docs/tutorials/web-search-wiline-inference/#without-web-search\" class=\"hash-link\" aria-label=\"Direct link to Without web search\" title=\"Direct link to Without web search\" translate=\"no\">​</a></h2>\n<p>A normal call, asking something recent:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> https://inference.wiline.com/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$WEC_API_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"model\": \"Qwen3.5-9B\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"messages\": [{\"role\": \"user\", \"content\": \"What is the latest stable version of Docker Engine, and when was it released?\"}]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  }'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The model answering from training data only — an outdated version\" src=\"https://development-wec.wiline.com/docs/assets/images/websearch-without-aeaee1a5d9a326ccb92d56a8ec08bdc5.png\" width=\"1099\" height=\"940\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> With no tool, the model won't commit to a version — it says its knowledge is\nlimited to its training cutoff and points you to the release notes instead.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"with-web-search\">With web search<a href=\"https://development-wec.wiline.com/docs/tutorials/web-search-wiline-inference/#with-web-search\" class=\"hash-link\" aria-label=\"Direct link to With web search\" title=\"Direct link to With web search\" translate=\"no\">​</a></h2>\n<p>The <strong>same</strong> request, plus a <code>web_search</code> tool and <code>tool_choice: auto</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sS</span><span class=\"token plain\"> https://inference.wiline.com/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$WEC_API_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"model\": \"Qwen3.5-9B\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"messages\": [{\"role\": \"user\", \"content\": \"What is the latest stable version of Docker Engine, and when was it released?\"}],</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"tools\": [{</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      \"type\": \"function\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      \"function\": {</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        \"name\": \"litellm_web_search\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        \"description\": \"Search the web for current information\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        \"parameters\": {</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          \"type\": \"object\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          \"properties\": {\"query\": {\"type\": \"string\", \"description\": \"The search query\"}},</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          \"required\": [\"query\"]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    }],</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"tool_choice\": \"auto\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  }'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The model answering with the current version, grounded in web results\" src=\"https://development-wec.wiline.com/docs/assets/images/websearch-with-ae28b4b9dbd33effc4676a36ce37a4e0.png\" width=\"1100\" height=\"731\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> Same model, same question, plus the tool — now it returns a concrete version\nand release date pulled from the live web. The model decided to search, WEC ran the query,\nand fed the results back before answering.</p>\n<p>That's the whole feature: one tool in the request body, current answers out.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"one-thing-to-watch-tokens\">One thing to watch: tokens<a href=\"https://development-wec.wiline.com/docs/tutorials/web-search-wiline-inference/#one-thing-to-watch-tokens\" class=\"hash-link\" aria-label=\"Direct link to One thing to watch: tokens\" title=\"Direct link to One thing to watch: tokens\" translate=\"no\">​</a></h2>\n<p>Web search injects the results into your prompt, so a searched call costs more tokens than\na bare one (in our test, prompt tokens went from ~26 to ~2,776). Keep <code>tool_choice: auto</code>\nso the model only searches when it actually needs to — it'll skip search for questions it\ncan already answer.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"when-to-use-it\">When to use it<a href=\"https://development-wec.wiline.com/docs/tutorials/web-search-wiline-inference/#when-to-use-it\" class=\"hash-link\" aria-label=\"Direct link to When to use it\" title=\"Direct link to When to use it\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>Yes:</strong> current versions, prices, news, anything after the model's training cutoff.</li>\n<li class=\"\"><strong>Skip:</strong> stable knowledge the model already has — <code>auto</code> handles that for you.</li>\n</ul>\n<p>Point it at a RAG assistant or a chatbot and it can stay current without you wiring up a\nseparate search service.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Add live web search</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>",
            "url": "https://development-wec.wiline.com/docs/tutorials/web-search-wiline-inference/",
            "title": "Add web search to your WEC Inference calls",
            "summary": "Your RAG assistant answers great from your docs — but not about anything recent. WEC Inference now has a built-in web-search tool: add it to a chat call and the model pulls in current info. A quick before/after test.",
            "date_modified": "2026-07-23T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "inference",
                "web-search",
                "tool-use"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/",
            "content_html": "<div class=\"tutorialHero\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2255\" height=\"527\" fill=\"none\" viewBox=\"0 0 2255 527\" class=\"tutorialHero__langfuse\"><path fill=\"#1B1917\" d=\"M652.669 116.433c0-10.261-7.683-17.956-17.926-17.956H607V60h87.923v316.366h-42.254zM804.056 379.786c-39.693 0-72.131-27.362-72.131-68.831 0-41.042 28.596-69.686 84.935-69.686h39.693c7.256 0 12.805-5.558 12.805-12.826v-8.55c0-25.651-20.487-38.905-40.547-38.905-18.353 0-33.291 8.123-41.828 26.934h-45.668c11.95-44.462 46.095-65.41 88.349-65.41 40.547 0 82.374 22.231 82.374 77.808v156.046h-42.68v-14.109c0-5.13-5.549-7.695-9.817-4.702-16.646 11.97-32.438 22.231-55.485 22.231m5.975-38.477c18.78 0 34.572-9.406 52.498-27.362 4.695-4.702 6.829-10.688 6.829-17.1v-6.413c0-7.268-5.549-12.826-12.805-12.826H815.58c-27.743 0-40.974 12.398-40.974 31.637 0 17.956 12.377 32.064 35.425 32.064M961.855 376.366V147.642h42.255v19.666c0 5.13 6.4 6.84 10.24 2.992 13.23-12.825 31.59-27.788 60.61-27.788 36.71 0 70.85 23.086 70.85 75.671v158.183h-42.25V227.161c0-29.499-17.93-45.745-39.7-45.745-20.91 0-35 11.971-49.93 29.499-7.26 8.978-9.82 19.238-9.82 30.354v135.097zM1287.48 467c-52.5 0-85.79-24.796-95.61-63.273h46.1c7.25 15.818 20.06 25.651 45.67 25.651 34.14 0 55.48-20.093 55.48-65.838v-7.268c0-5.13-4.27-7.695-9.39-3.42-14.08 12.398-32.01 20.093-49.08 20.093-58.05 0-96.46-44.889-96.46-115.003 0-70.113 44.39-115.43 98.17-115.43 15.79 0 30.3 4.275 44.38 14.963 5.98 4.275 12.38.855 12.38-5.986v-3.847h42.26V363.54c0 72.678-43.97 103.46-93.9 103.46m-2.56-132.959c19.2 0 33.29-8.55 44.81-20.949 7.26-8.122 9.39-13.68 9.39-26.506v-61.563c0-12.826-2.13-20.948-10.67-29.071-9.39-8.978-22.62-14.964-39.69-14.964-35 0-61.89 29.072-61.89 76.954 0 47.883 24.76 76.099 58.05 76.099M1455.92 199.372c0-7.268-5.97-13.253-13.23-13.253h-32.44v-38.477h32.44c7.26 0 13.23-5.985 13.23-13.253v-5.986c0-45.744 23.48-68.403 69.15-68.403h29.02v38.477h-29.45c-17.5 0-26.46 9.833-26.46 29.926v5.986c0 7.268 5.97 13.253 13.23 13.253h42.68v38.477h-42.68c-7.26 0-13.23 5.985-13.23 13.253v176.994h-42.26zM1652.02 381.496c-35.85 0-69.14-23.086-69.14-75.671V147.642h42.25v150.06c0 29.499 17.07 44.889 37.13 44.889 21.77 0 35.85-11.97 50.79-29.499 7.26-8.977 9.82-19.238 9.82-30.354V147.642h42.25v228.724h-42.25V356.7c0-5.131-6.4-6.841-10.24-2.993-13.24 12.826-31.59 27.789-60.61 27.789M1893.57 381.496c-38.84 0-79.39-19.239-90.06-65.838h43.54c6.4 17.528 23.9 29.498 44.81 29.498 23.05 0 37.13-13.68 37.13-30.353 0-16.246-11.09-25.224-28.59-30.354l-36.28-10.261c-31.58-8.978-55.06-29.499-55.06-64.556 0-38.049 35.43-67.12 75.55-67.12 32.01 0 70.85 14.535 81.09 65.41h-40.55c-5.55-17.528-20.06-29.071-40.54-29.071-20.06 0-34.58 12.398-34.58 28.216 0 13.253 8.11 23.086 27.32 28.644l34.15 9.833c32.43 9.406 58.47 29.072 58.47 66.693 0 39.332-34.15 69.259-76.4 69.259M2098.54 381.496c-61.46 0-102.01-51.73-102.01-119.706s43.11-119.278 101.58-119.278c63.6 0 96.89 51.302 96.89 109.872v23.087h-144.26c-5.98 0-8.54 3.847-7.26 13.68 4.7 32.064 30.31 54.295 55.49 54.295 18.78 0 35-9.405 45.67-27.788h44.81c-16.22 40.187-49.94 65.838-90.91 65.838m43.11-141.51c6.83 0 9.39-3.42 7.68-14.108-4.69-26.506-24.33-45.317-51.22-45.317-25.6 0-47.37 18.811-54.2 45.745-2.56 9.833.85 13.68 6.83 13.68z\"></path><path fill=\"#FF5D5F\" d=\"m286.292 286.105 34.597 27.791s26.473-19.661 45.941-22.545c20.418-3.025 42.202 8.359 62.388 21.93 30.489 20.498 56.149 46.508 56.149 46.508l30.06-29.493s-82.879-89.795-148.597-81.672c-43.105 5.328-80.538 37.481-80.538 37.481\"></path><path fill=\"#4E9CFF\" d=\"M88.358 114.862 60 146.056s79.009 73.732 141.224 73.732c28.358 0 67.684-22.216 101.523-51.079 19.283-16.448 40.835-35.13 62.388-35.13 14.487 0 33.594 7.673 51.612 27.824 0 0 11.63-6.974 18.716-11.985 6.228-4.404 15.479-11.91 15.479-11.91-25.918-27.663-63.407-47.883-85.807-45.9-36.299.005-62.388 22.601-94.717 48.735s-45.94 36.907-69.194 36.907c-39.134 0-112.866-62.388-112.866-62.388M88.358 352.463 60 321.269s79.009-73.732 141.224-73.732c28.358 0 67.684 22.216 101.523 51.079 19.283 16.448 40.835 35.13 62.388 35.13 14.556 0 33.518-7.989 51.612-28.358 0 0 10.877 6.705 17.582 11.344 6.894 4.769 17.015 12.655 17.015 12.655-25.931 27.883-63.693 48.323-86.209 46.33-36.299-.005-57.851-19.24-90.179-45.374-32.329-26.133-50.478-40.268-73.732-40.268-39.134 0-112.866 62.388-112.866 62.388M458.142 185.149c-7.378 5.1-19.283 12.478-19.283 12.478s6.806 14.746 6.806 34.597-6.239 36.866-6.239 36.866 10.688 6.675 17.582 11.343c7.162 4.849 18.149 13.045 18.149 13.045s13.045-27.224 13.045-61.254-13.045-59.552-13.045-59.552-10.236 7.792-17.015 12.477\"></path><path fill=\"#FF5D5F\" d=\"m287.995 180.612 32.895-27.224s26.473 19.046 45.941 21.93c20.417 3.026 42.202-8.359 62.388-21.93 30.489-20.498 56.149-46.507 56.149-46.507l30.06 29.492s-82.879 89.795-148.597 81.672c-43.105-5.328-78.836-37.433-78.836-37.433M208.601 91c42.538 0 78.264 36.299 78.264 36.299s-9.941 7.832-16.448 13.045c-6.777 5.429-17.582 14.179-17.582 14.179s-18.711-19.851-44.234-19.851c-10.465 0-24.066 6.286-38.567 18.716-11.188 9.591-22.829 21.514-30.627 36.299-6.743 12.784-10.42 27.85-10.776 43.672-.446 19.873 6.597 40.704 18.149 57.283 7.743 11.112 16.983 19.474 26.657 26.657 12.555 9.322 25.648 15.881 35.164 15.881 10.166 0 19.306-3.533 26.09-6.806 10.776-6.239 19.278-13.612 19.278-13.612l33.463 27.791s-13.612 13.612-32.323 23.821c-12.091 5.963-27.632 11.91-46.508 11.91-18.862 0-40.767-10.022-61.254-25.522-13.244-10.021-26.225-21.895-36.298-36.299-16.51-23.607-25.017-52.328-24.96-81.104.057-29.136 9.451-57.993 26.094-81.672C138.273 117.657 176.86 91 208.601 91\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 8 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->8</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->8<!-- --> earned</span></div><div class=\"skillTracker__series\">AI evals &amp; observability</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove a model works</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trustworthy JSON</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real test data at scale</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Observe &amp; score production</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">RAG, end to end</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">Catch regressions in CI</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">7</span><span class=\"skillTracker__skill\" data-state=\"current\">Trace &amp; debug agent tool calls</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/web-search-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Add live web search</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>An agent that calls tools does two very different things: it <strong>reasons</strong> (\"I should\nsearch the docs\") and it <strong>acts</strong> (actually calls the tool). When something goes wrong,\nthe first question is always <em>which layer failed</em> — did it think wrong, or did the doing\nbreak? A flat log can't answer that. A <strong>trace</strong> can.</p>\n<p>In this tutorial you build a small tool-calling agent on <strong>WEC Inference</strong>, instrument it\nwith <strong>Langfuse</strong> so every reasoning step and every tool call becomes an inspectable node,\nthen debug <strong>two</strong> real failures from the trace tree — including the worst kind: a\nconfident, wrong answer that never throws an error. Every command, error, and screenshot\nbelow is from a real run.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>Built on a WEC Instance with Docker, against <strong>WEC Inference</strong> (<code>Qwen3.5-122B</code>) and a\nself-hosted <strong>Langfuse</strong> from the\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/\">observability tutorial</a>. The agent's tools\ninclude the RAG service from the\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/\">RAG tutorial</a> — so this piece ties the\nwhole series together. All model calls stay on WEC; nothing leaves for a third-party API.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-youll-build\">What you'll build<a href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/#what-youll-build\" class=\"hash-link\" aria-label=\"Direct link to What you'll build\" title=\"Direct link to What you'll build\" translate=\"no\">​</a></h2>\n<!-- -->\n<p>The dotted lines are the point: every step reports itself to Langfuse, so the agent's\n<strong>reasoning</strong> and its <strong>actions</strong> land as separate, inspectable nodes.</p>\n<p><strong>Prerequisites:</strong> a WEC Instance with Docker, a <strong>WEC Inference API key</strong>, a running\nLangfuse (public + secret key, host URL), and a tool the agent can call — here the RAG\n<code>/ask</code> service. All commands run in <code>~/agent-tracing</code>.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--see-the-two-layers-no-tracing-yet\">Step 1 — See the two layers (no tracing yet)<a href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/#step-1--see-the-two-layers-no-tracing-yet\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — See the two layers (no tracing yet)\" title=\"Direct link to Step 1 — See the two layers (no tracing yet)\" translate=\"no\">​</a></h2>\n<p><strong>Rule: never instrument before you've seen the raw behavior.</strong> So v1 does the minimum —\nask the model a question <em>with a tool definition</em>, and print what comes back. Set up the\nworkspace and the WEC key:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> ~/agent-tracing </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/agent-tracing</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'^WEC_API_KEY='</span><span class=\"token plain\"> ~/evolution-api/.env </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> .env</span><br></div></code></pre></div></div>\n<p><code>agent.py</code> (v1):</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/agent-tracing/agent.py (v1)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> requests</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WEC_URL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"https://inference.wiline.com/v1/chat/completions\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WEC_API_KEY </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"WEC_API_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MODEL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Qwen3.5-122B\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">TOOLS </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"function\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"function\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"name\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"search_docs\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"description\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Search the WEC documentation for an answer to a question.\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"parameters\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"object\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token string\" style=\"color:#e3116c\">\"properties\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"query\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"string\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token string\" style=\"color:#e3116c\">\"required\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"query\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">call_model</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">messages</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> requests</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">post</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">WEC_URL</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        headers</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Bearer </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">WEC_API_KEY</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        json</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"model\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> MODEL</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> messages</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"tools\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> TOOLS</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"tool_choice\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"auto\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        timeout</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">120</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">raise_for_status</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"choices\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"message\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> __name__ </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"__main__\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    msg </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> call_model</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"user\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"How do I create a compute instance on WEC?\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"=== REASONING LAYER (what it thought) ===\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">msg</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"reasoning_content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"(none)\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"\\n=== ACTION LAYER (what it decided to do) ===\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> tc </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> msg</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"tool_calls\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"tool: </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">tc</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'function'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'name'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">  args: </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">tc</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'function'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'arguments'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Run it in a container (same pattern as the rest of the series):</p>\n<div class=\"language-docker codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/agent-tracing/Dockerfile</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-docker codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FROM python:3.12-slim</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WORKDIR /app</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">RUN pip install --no-cache-dir requests</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">COPY agent.py .</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">CMD [\"python\", \"agent.py\"]</span><br></div></code></pre></div></div>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/agent-tracing/docker-compose.yml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">services</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">agent</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">build</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> .</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">env_file</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> .env</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--build</span><span class=\"token plain\"> agent</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The model&amp;#39;s reasoning and its tool choice printed as two separate blocks\" src=\"https://development-wec.wiline.com/docs/assets/images/agent-reasoning-action-56b6f9e7c5b8aef031cd06c04187e291.png\" width=\"862\" height=\"630\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> WEC Inference returns the two layers <em>in one response</em>: <code>reasoning_content</code>\n(the thinking) and <code>tool_calls</code> (the decision). We don't have to infer the split — the API\nhands it to us.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>WEC Inference supports tool calling — and exposes the reasoning</div><div class=\"admonitionContent_BuS1\"><p><code>Qwen3.5-122B</code> returns <code>finish_reason: tool_calls</code> with a proper <code>tool_calls</code> array, <strong>plus\na <code>reasoning_content</code> field</strong> containing the model's chain of thought before it acts. That\nreasoning field is exactly what makes the reasoning-vs-action split visible later.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--close-the-loop-execute-the-tool-get-the-answer\">Step 2 — Close the loop: execute the tool, get the answer<a href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/#step-2--close-the-loop-execute-the-tool-get-the-answer\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Close the loop: execute the tool, get the answer\" title=\"Direct link to Step 2 — Close the loop: execute the tool, get the answer\" translate=\"no\">​</a></h2>\n<p>v1 <em>decided</em> to search but didn't. Now actually call the tool (the RAG <code>/ask</code> service),\nfeed the result back, and let the model write the final answer. Add the tool + the loop:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/agent-tracing/agent.py (v2, additions)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">RAG_URL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://&lt;your-vm-ip&gt;:8000/ask\"</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># the RAG service = our one tool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">search_docs</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">query</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> requests</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">post</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">RAG_URL</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> json</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"question\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> query</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> timeout</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">120</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">raise_for_status</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"answer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">user_input</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    messages </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"user\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> user_input</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">while</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        msg </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> call_model</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">messages</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> msg</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"tool_calls\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"\\n=== FINAL ANSWER ===\\n\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">+</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">msg</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> msg</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        messages</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">append</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"assistant\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> msg</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                         </span><span class=\"token string\" style=\"color:#e3116c\">\"tool_calls\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> msg</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"tool_calls\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> tc </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> msg</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"tool_calls\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            args </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">loads</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">tc</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"function\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"arguments\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"[reasoning] </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation\">msg</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">get</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'reasoning_content'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation\"> </span><span class=\"token string-interpolation interpolation keyword\" style=\"color:#00009f\">or</span><span class=\"token string-interpolation interpolation\"> </span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">''</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">strip</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">)</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">140]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"[action]    </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">tc</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'function'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'name'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">(</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">args</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">)\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            result </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> search_docs</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">args</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"query\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"[tool result] </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">result</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">140]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">...\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            messages</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">append</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"tool\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"tool_call_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> tc</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> result</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--build</span><span class=\"token plain\"> agent</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The full agent loop: reasoning, action, tool result, then the final grounded answer\" src=\"https://development-wec.wiline.com/docs/assets/images/agent-loop-b09953660869437421c9222da3f9b135.png\" width=\"862\" height=\"783\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> The complete loop — the model reasons, calls <code>search_docs</code>, gets the RAG\nresult, and writes a grounded answer. It works. But a working terminal tells you <em>nothing</em>\nabout whether the reasoning was sound — that's what we fix next.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--add-langfuse-the-layers-become-a-trace-tree\">Step 3 — Add Langfuse: the layers become a trace tree<a href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/#step-3--add-langfuse-the-layers-become-a-trace-tree\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — Add Langfuse: the layers become a trace tree\" title=\"Direct link to Step 3 — Add Langfuse: the layers become a trace tree\" translate=\"no\">​</a></h2>\n<p>The terminal flattens everything into one stream. Langfuse turns each function into a\n<strong>node</strong> so you can see the structure. Reuse the Langfuse keys from the observability\ntutorial (same project as your RAG traces):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-E</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'^LANGFUSE_(PUBLIC_KEY|SECRET_KEY|HOST)='</span><span class=\"token plain\"> ~/rag-service/.env </span><span class=\"token operator\" style=\"color:#393A34\">&gt;&gt;</span><span class=\"token plain\"> .env</span><br></div></code></pre></div></div>\n<p>Add the SDK (<code>pip install ... langfuse</code>) and decorate three functions — that's the whole\ninstrumentation. <code>@observe</code> wraps a function as a span; <code>as_type=\"generation\"</code> marks the\nLLM calls so Langfuse captures model, tokens, and cost:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/agent-tracing/agent.py (v3, decorators)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langfuse </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> observe</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> get_client</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@observe</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">as_type</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"generation\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">call_model</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">messages</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    data </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    get_client</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">update_current_generation</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">MODEL</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> usage_details</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">data</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"usage\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> data</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"choices\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"message\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@observe</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">search_docs</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">query</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@observe</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">user_input</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> __name__ </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"__main__\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    run</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"How do I create a compute instance on WEC?\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    get_client</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">flush</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># short-lived script: push traces before exit</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--build</span><span class=\"token plain\"> agent</span><br></div></code></pre></div></div>\n<p>Open Langfuse → <strong>Tracing → Traces</strong>. First thing you notice: there are <strong>two</strong> traces per\nquestion.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Langfuse traces list showing a run trace and an ask trace\" src=\"https://development-wec.wiline.com/docs/assets/images/trace-list-3a0d985919db7d12160c4109ab85dede.png\" width=\"1901\" height=\"763\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> Distributed tracing, for free: your agent's <code>run</code> trace and the RAG service's\nown <code>ask</code> trace are <em>separate</em> — each service instruments itself. Seeing both is how you\nfollow one request across service boundaries.</p>\n<p>Open the newest <code>run</code>:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Langfuse trace tree: run, two call_model generations, and the search_docs span\" src=\"https://development-wec.wiline.com/docs/assets/images/trace-tree-green-c19c35d6b1ba42189355bdd0e79d96f8.png\" width=\"1902\" height=\"996\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 4.</strong> The agent turn as a tree: <code>run</code> → <code>call_model</code> (decide) → <code>search_docs</code>\n(act) → <code>call_model</code> (answer). Langfuse also captured <strong>latency</strong>, <strong>token usage</strong>\n(801→306), and a correctness <strong>score</strong> — all for three decorators.</p>\n<p>Click the first <code>call_model</code> and open its output:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The call_model node showing reasoning_content and tool_calls in Langfuse\" src=\"https://development-wec.wiline.com/docs/assets/images/trace-reasoning-content-c5e9876f6e9a640d45e9f5f54c5594cc.png\" width=\"1899\" height=\"996\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 5.</strong> The reasoning layer, captured: <code>reasoning_content</code> (\"<em>I should search the WEC\ndocumentation…</em>\") sits right next to the <code>tool_calls</code> it produced. This is the record you\ndebug against.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--break-it-the-loud-way-then-debug-from-the-trace\">Step 4 — Break it (the loud way), then debug from the trace<a href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/#step-4--break-it-the-loud-way-then-debug-from-the-trace\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Break it (the loud way), then debug from the trace\" title=\"Direct link to Step 4 — Break it (the loud way), then debug from the trace\" translate=\"no\">​</a></h2>\n<p>Plant a <strong>realistic</strong> bug: the RAG API expects <code>{\"question\": ...}</code>, but it's natural to\nsend <code>{\"query\": ...}</code> because the <em>tool's</em> parameter is named <code>query</code>. One wrong key:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/{\"question\": query}/{\"query\": query}/'</span><span class=\"token plain\"> agent.py</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--build</span><span class=\"token plain\"> agent</span><br></div></code></pre></div></div>\n<p>It crashes:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The terminal showing a 422 HTTPError traceback\" src=\"https://development-wec.wiline.com/docs/assets/images/agent-422-crash-af97e2fe3ae5709ec3fe6d5f859095a0.png\" width=\"859\" height=\"862\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 6.</strong> The terminal gives you a <strong>stack trace</strong> — it tells you <em>where the code\nbroke</em>. It does <strong>not</strong> tell you whether the agent <em>thought</em> correctly. For that, go to the\ntrace.</p>\n<p>Open the failed <code>run</code> and click the red <code>search_docs</code> span:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The broken trace: call_model green, search_docs red with a 422 error\" src=\"https://development-wec.wiline.com/docs/assets/images/trace-error-span-ddca621bf15a8cc64c96371cd6a42e1a.png\" width=\"1901\" height=\"997\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 7.</strong> The whole diagnosis in one picture: <code>call_model</code> is <strong>green</strong> (the reasoning\nwas right — it correctly chose to search), <code>search_docs</code> is <strong>red</strong>, died in <strong>0.04s</strong>, and\nthe panel shows the <code>422</code> plus the exact payload it sent. An instant failure is a\n<em>rejection</em>, not a timeout — pointing straight at a bad request.</p>\n<p>Fix the one line and confirm green:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/{\"query\": query}/{\"question\": query}/'</span><span class=\"token plain\"> agent.py</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--build</span><span class=\"token plain\"> agent</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The fixed trace: all nodes green, final answer produced\" src=\"https://development-wec.wiline.com/docs/assets/images/trace-fixed-green-c3b4677676dc40f861fca6dafe1594bb.png\" width=\"1896\" height=\"997\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 8.</strong> Back to green — <code>search_docs</code> succeeds and the final answer is produced. You\ndiagnosed <em>and</em> verified the fix from the trace.</p>\n<p>That was the <strong>loud</strong> failure — it crashed, so you'd have caught it eventually even without\na trace. The dangerous one is next.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--a-second-tool-so-the-agent-has-to-choose\">Step 5 — A second tool, so the agent has to <em>choose</em><a href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/#step-5--a-second-tool-so-the-agent-has-to-choose\" class=\"hash-link\" aria-label=\"Direct link to step-5--a-second-tool-so-the-agent-has-to-choose\" title=\"Direct link to step-5--a-second-tool-so-the-agent-has-to-choose\" translate=\"no\">​</a></h2>\n<p>Real agents have more than one tool, and the interesting decisions happen when the model\npicks between them. Add a <code>get_pricing</code> tool alongside <code>search_docs</code>, and route calls\nthrough a <strong>dispatch table</strong> keyed by tool name:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/agent-tracing/agent.py (v4, two tools + dispatch)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">TOOLS </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"function\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"function\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token string\" style=\"color:#e3116c\">\"name\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"search_docs\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token string\" style=\"color:#e3116c\">\"description\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Search the WEC documentation for how-to and setup questions.\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token string\" style=\"color:#e3116c\">\"parameters\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"object\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"properties\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"query\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"string\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"required\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"query\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"function\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"function\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token string\" style=\"color:#e3116c\">\"name\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"get_pricing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token string\" style=\"color:#e3116c\">\"description\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Get the price of a WEC resource (e.g. 'compute instance', 'block storage', 'inference').\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token string\" style=\"color:#e3116c\">\"parameters\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"object\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"properties\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"resource\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"string\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"required\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"resource\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">PRICES </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"compute instance\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"$0.03 / vCPU-hour\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"block storage\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"$0.10 / GB-month\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"inference\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"$0.50 per 1M tokens\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@observe</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">get_pricing</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">resource</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> PRICES</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">resource</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">lower</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">strip</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"No pricing found for '</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">resource</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">'.\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">TOOL_FUNCS </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"search_docs\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> search_docs</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"get_pricing\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> get_pricing</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># in run(), dispatch by the tool the model actually chose:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> tc </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> msg</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"tool_calls\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    name </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> tc</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"function\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"name\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    args </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">loads</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">tc</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"function\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"arguments\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"[action] </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">name</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">(</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">args</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">)\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    result </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">TOOL_FUNCS</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token operator\" style=\"color:#393A34\">**</span><span class=\"token plain\">args</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    messages</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">append</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"tool\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"tool_call_id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> tc</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"id\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> result</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Ask a <strong>pricing</strong> question (<code>run(\"How much does a compute instance cost on WEC?\")</code>) and run\nit. The agent should pick <code>get_pricing</code>, not <code>search_docs</code>:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Langfuse trace where the agent correctly chose get_pricing\" src=\"https://development-wec.wiline.com/docs/assets/images/trace-two-tools-ba891f9c0ec4d99aea2e499dcbba3189.png\" width=\"1899\" height=\"995\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 9.</strong> Genuine tool selection: given two tools, the agent chose <code>get_pricing</code> and\nanswered <strong>$0.03/vCPU-hour</strong>, Correctness 1.00. The choice itself is now a thing you can\nsee in the trace.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-6--the-silent-failure-why-tracing-earns-its-keep\">Step 6 — The silent failure (why tracing earns its keep)<a href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/#step-6--the-silent-failure-why-tracing-earns-its-keep\" class=\"hash-link\" aria-label=\"Direct link to Step 6 — The silent failure (why tracing earns its keep)\" title=\"Direct link to Step 6 — The silent failure (why tracing earns its keep)\" translate=\"no\">​</a></h2>\n<p>Now a bug that <strong>doesn't crash</strong>. Someone refactors the dispatch and hardcodes the old\nsingle tool, forgetting the new one — so every call runs <code>search_docs</code> no matter what the\nmodel chose:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/result = str(TOOL_FUNCS\\[name\\](\\*\\*args))/result = str(search_docs(list(args.values())[0]))  # BUG/'</span><span class=\"token plain\"> agent.py</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--build</span><span class=\"token plain\"> agent</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">A passing run — with a completely wrong answer</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[action] get_pricing({'resource': 'compute instance'})</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">=== FINAL ANSWER ===</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Based on the pricing information available, the highest cost compute instance category on</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WEC is $1,586.77. ...</span><br></div></code></pre></div></div>\n<p>Read that carefully. The model <strong>chose <code>get_pricing</code></strong> (correct). The answer is confident,\ndetailed — and <strong>completely wrong</strong> (the real price is $0.03/vCPU-hour). There is <strong>no\nerror, no traceback.</strong> A log would look perfectly healthy. This is the failure mode that\nships to production and quietly lies to users.</p>\n<p>The trace is the only thing that catches it. Open the <code>run</code>:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The silent-failure trace: get_pricing chosen but search_docs executed, Correctness 0.00\" src=\"https://development-wec.wiline.com/docs/assets/images/trace-silent-failure-162a3ce9394bad7caca0e908be683b67.png\" width=\"1901\" height=\"996\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 10.</strong> Three tells a log can't show: (1) the executed span is <strong><code>search_docs</code></strong> —\nthere's <strong>no <code>get_pricing</code> span</strong>, even though the model chose it: <em>decision ≠ action</em>;\n(2) the reasoning node scored <strong>Correctness 0.00</strong> — the LLM-judge auto-flagged the wrong\nanswer; (3) the output shows the model <em>confused</em> by a tool result that didn't match what\nit asked for. Fix the dispatch back to <code>TOOL_FUNCS[name](**args)</code> and the price is correct\nagain.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You instrumented a tool-calling agent so its reasoning and its actions are separate,\ninspectable trace nodes — and debugged <strong>two</strong> real failures from the trace: the loud\ncrash <em>and</em> the silent, confident-but-wrong answer that no stack trace would ever reveal.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting\">Troubleshooting<a href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/#troubleshooting\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting\" title=\"Direct link to Troubleshooting\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>No trace appears.</strong> A short script exits before the SDK flushes. Call\n<code>get_client().flush()</code> before the process ends (already in v3).</li>\n<li class=\"\"><strong><code>search_docs</code> span missing.</strong> Only <code>@observe</code>-decorated functions become nodes —\ndecorate the function, not the call site.</li>\n<li class=\"\"><strong>Generation shows no tokens/cost.</strong> Pass <code>usage_details=data.get(\"usage\")</code> via\n<code>update_current_generation</code>; without it Langfuse can't compute cost.</li>\n<li class=\"\"><strong>Agent and tool traces look disconnected.</strong> Expected — each service traces itself. Link\nthem later with trace propagation if you need a single cross-service view.</li>\n<li class=\"\"><strong>A silent wrong answer with no error.</strong> Compare the model's <code>tool_calls</code> (what it chose)\nagainst the executed spans (what actually ran). A mismatch is a dispatch/routing bug — and\na correctness score on the trace will flag it automatically.</li>\n</ul>\n<hr>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Trace &amp; debug agent tool calls</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>You can now see an agent think, act, and <em>fail quietly</em> — and catch it. Point the same\ninstrumentation at a real multi-step agent, add automated correctness scoring on every\ntrace, and you have production observability that surfaces silent failures before your users\ndo.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"teardown\">Teardown<a href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/#teardown\" class=\"hash-link\" aria-label=\"Direct link to Teardown\" title=\"Direct link to Teardown\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/agent-tracing </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose down </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rmi</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">local</span><br></div></code></pre></div></div>",
            "url": "https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/",
            "title": "Component-level tracing: debugging agent tool calls",
            "summary": "Your agent's thinking and its actions are two different layers. Build a tool-calling agent on WEC Inference, trace it with Langfuse, then debug two real failures from the trace — including the confident, wrong answer that never throws a stack trace.",
            "date_modified": "2026-07-22T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "evals",
                "observability",
                "langfuse",
                "agents",
                "tracing",
                "inference"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/whatsapp-ai-assistant-evolution-api/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/whatsapp_logo-cc34546fcff90d36128c7230c48c496f.png\" alt=\"WhatsApp\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__evolution\" src=\"https://development-wec.wiline.com/docs/assets/images/0_hT2ECga7Z67dEbYz-62f9ab60d339ae8868fee46475e505bc.png\" alt=\"Evolution\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 2 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->2</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->2<!-- --> earned</span></div><div class=\"skillTracker__series\">WhatsApp automation on WEC</div><ul class=\"skillTracker__steps\"><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">1</span><span class=\"skillTracker__skill\" data-state=\"current\">Self-host a WhatsApp AI bridge</span></span></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Production delivery via Cloud API</span></span></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Most \"self-host a WhatsApp AI\" guides stop at \"the container started.\" This one goes all\nthe way: you deploy a real, programmable WhatsApp gateway (<strong>Evolution API</strong>), then\n<strong>write the bridge yourself</strong> — the ~50 lines that turn an incoming message into an LLM\nanswer and send it back. That bridge (webhook → model → reply) is the reusable pattern\nbehind <em>every</em> chat-AI integration: SMS, Slack, Telegram, voice — swap the channel, the\nshape is identical.</p>\n<p>And because this is a real build, we hit — and fix — every gotcha: an image that moved\npublishers, a Baileys version loop, an infinite reply loop, group-chat spam, WhatsApp's\nnew <strong>LID</strong> addressing, and a genuine <strong>delivery wall</strong> that most tutorials pretend\ndoesn't exist. Every command, error, and output below is from an actual run.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Read this before you use your personal number</div><div class=\"admonitionContent_BuS1\"><p>Evolution links to a WhatsApp number as a <strong>companion device</strong> (like WhatsApp Web) — the\nbot acts <em>as that account</em> and can read every DM and group it receives. For a demo on a\nnumber you control it's fine; for anything real, use a <strong>dedicated number</strong>. And see the\n<a href=\"https://development-wec.wiline.com/docs/tutorials/whatsapp-ai-assistant-evolution-api/#the-delivery-wall\" class=\"\">delivery limitation</a> at the end before you build on this in\nproduction.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-youll-build\">What you'll build<a href=\"https://development-wec.wiline.com/docs/tutorials/whatsapp-ai-assistant-evolution-api/#what-youll-build\" class=\"hash-link\" aria-label=\"Direct link to What you'll build\" title=\"Direct link to What you'll build\" translate=\"no\">​</a></h2>\n<!-- -->\n<p>Four containers: Evolution + its Postgres + Redis, and the <strong>bridge</strong> you'll write. The\nbridge is the whole point — everything else is off-the-shelf.</p>\n<p><strong>Prerequisites:</strong> a WEC Instance with Docker + Compose, a <strong>WEC Inference API key</strong>\n(<a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/inference/api_keys/\">Inference → API Keys</a>), a WhatsApp number for\nthe demo, and ~2 GB free disk.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--deploy-the-evolution-stack\">Step 1 — Deploy the Evolution stack<a href=\"https://development-wec.wiline.com/docs/tutorials/whatsapp-ai-assistant-evolution-api/#step-1--deploy-the-evolution-stack\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Deploy the Evolution stack\" title=\"Direct link to Step 1 — Deploy the Evolution stack\" translate=\"no\">​</a></h2>\n<p>Evolution needs its own Postgres and Redis. We give it a dedicated set on an internal\nnetwork and expose only the API port (8080). <code>docker-compose.yml</code>:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/evolution-api/docker-compose.yml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">services</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">evolution-api</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> evoapicloud/evolution</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">api</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">v2.2.3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">restart</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> unless</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">stopped</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">ports</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"8080:8080\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">env_file</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> .env</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">volumes</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> evolution_instances</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/evolution/instances</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">depends_on</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">evolution</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">postgres</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> evolution</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">redis</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">logging</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">driver</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">file</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">options</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">max-size</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"50m\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">max-file</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"3\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">evolution-postgres</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> postgres</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">17</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">restart</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> unless</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">stopped</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">environment</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">POSTGRES_USER</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> evolution</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">POSTGRES_PASSWORD</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">POSTGRES_PASSWORD</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">POSTGRES_DB</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> evolution</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">volumes</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> evolution_pgdata</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/var/lib/postgresql/data</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">evolution-redis</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> redis</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">7</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">restart</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> unless</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">stopped</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">volumes</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> evolution_redis</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/data</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token key atrule\" style=\"color:#00a4db\">volumes</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">evolution_instances</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">evolution_pgdata</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  evolution_redis</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Log rotation from day one</div><div class=\"admonitionContent_BuS1\"><p>The <code>logging:</code> block caps container logs at 3×50 MB. Skip it and a chatty container can\nfill the disk and take the box down — a lesson learned the hard way on this VM. Set it\nbefore you need it.</p></div></div>\n<p>The <code>.env</code> (generated secrets, connection URIs, and — importantly — a <strong>pinned Baileys\nWhatsApp-Web version</strong>):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Generate the .env</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/evolution-api </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">AUTHENTICATION_API_KEY=</span><span class=\"token string variable\" style=\"color:#36acaa\">$(</span><span class=\"token string variable\" style=\"color:#36acaa\">openssl rand </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-hex</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable number\" style=\"color:#36acaa\">24</span><span class=\"token string variable\" style=\"color:#36acaa\">)</span><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">SERVER_URL=http://&lt;your-vm-ip&gt;:8080</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">POSTGRES_PASSWORD=</span><span class=\"token string variable\" style=\"color:#36acaa\">$(</span><span class=\"token string variable\" style=\"color:#36acaa\">openssl rand </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-hex</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable number\" style=\"color:#36acaa\">16</span><span class=\"token string variable\" style=\"color:#36acaa\">)</span><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">DATABASE_ENABLED=true</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">DATABASE_PROVIDER=postgresql</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">DATABASE_CONNECTION_URI=postgresql://evolution:\\</span><span class=\"token string variable\" style=\"color:#36acaa\">${POSTGRES_PASSWORD}</span><span class=\"token string\" style=\"color:#e3116c\">@evolution-postgres:5432/evolution?schema=public</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">CACHE_REDIS_ENABLED=true</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">CACHE_REDIS_URI=redis://evolution-redis:6379/0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">CACHE_REDIS_PREFIX_KEY=evolution</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">CACHE_LOCAL_ENABLED=false</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">QRCODE_LIMIT=30</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div></code></pre></div></div>\n<p>Bring it up:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">15</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://localhost:8080 </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">{</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"status\": 200,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"message\": \"Welcome to the Evolution API, it is working!\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"version\": \"2.2.3\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Evolution API responding on port 8080 with the welcome payload\" src=\"https://development-wec.wiline.com/docs/assets/images/wa-evolution-up-680a7f5fe9e4a450cacf33f1df5ed6f7.png\" width=\"628\" height=\"182\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> The stack is up: Evolution answers on <code>:8080</code> with its version and status.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Two real gotchas pulling the image</div><div class=\"admonitionContent_BuS1\"><p><strong>(1) The publisher moved.</strong> The image most guides reference — <code>atendai/evolution-api</code> —\nnow returns <em>\"pull access denied / repository does not exist.\"</em> The current image is\n<strong><code>evoapicloud/evolution-api</code></strong>. The old repo's tag list still resolves, which sends you\ndown a rabbit hole; confirm with a fresh pull of <code>hello-world</code> that it's not rate-limiting.\n<strong>(2) Pin a real tag.</strong> <code>v2.1.1</code> (from a popular blog) was never published — check\n<code>https://hub.docker.com/v2/repositories/evoapicloud/evolution-api/tags</code> and pin one that\nexists (we use <code>v2.2.3</code>).</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--create-an-instance-and-link-whatsapp\">Step 2 — Create an instance and link WhatsApp<a href=\"https://development-wec.wiline.com/docs/tutorials/whatsapp-ai-assistant-evolution-api/#step-2--create-an-instance-and-link-whatsapp\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Create an instance and link WhatsApp\" title=\"Direct link to Step 2 — Create an instance and link WhatsApp\" translate=\"no\">​</a></h2>\n<p>Set your API key as a shell var (every request needs it in an <code>apikey:</code> header):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">APIKEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">grep</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'^AUTHENTICATION_API_KEY='</span><span class=\"token variable\" style=\"color:#36acaa\"> .env </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable function\" style=\"color:#d73a49\">cut</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token variable operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-f2</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><br></div></code></pre></div></div>\n<p>Create the instance:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST http://localhost:8080/instance/create </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"apikey: </span><span class=\"token string variable\" style=\"color:#36acaa\">$APIKEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"instanceName\":\"wec-demo\",\"integration\":\"WHATSAPP-BAILEYS\",\"qrcode\":true}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'.instance'</span><br></div></code></pre></div></div>\n<p>Then open the built-in Manager at <code>http://&lt;your-vm-ip&gt;:8080/manager</code>, enter your server\nURL + API key, click <strong>wec-demo</strong>, and scan the QR from <strong>WhatsApp → Settings → Linked\nDevices → Link a Device</strong>. Confirm it's connected:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> http://localhost:8080/instance/fetchInstances </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"apikey: </span><span class=\"token string variable\" style=\"color:#36acaa\">$APIKEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'.[] | {name, connectionStatus}'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">{ \"name\": \"wec-demo\", \"connectionStatus\": \"open\" }</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Evolution Manager showing the wec-demo instance connected\" src=\"https://development-wec.wiline.com/docs/assets/images/wa-manager-connected-d91fda82b98c6238d0d747b47636645b.png\" width=\"2834\" height=\"1468\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> The Evolution Manager dashboard once the instance links: <code>Connected</code>, with live contact, chat, and message counts.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>The Baileys version loop — the #1 self-host failure</div><div class=\"admonitionContent_BuS1\"><p>If the instance never leaves <code>connecting</code> and the logs show <code>ChannelStartupService</code>\nre-initializing every few seconds with a <code>Baileys version env: 2,3000,...</code> line, the\npinned WhatsApp-Web version is <strong>stale</strong> and WhatsApp rejects the handshake before a QR is\never generated. Fetch the current version and pin it, then recreate:</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"https://raw.githubusercontent.com/WhiskeySockets/Baileys/master/src/Defaults/baileys-version.json\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># {\"version\":[2,3000,1035194821]}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"CONFIG_SESSION_PHONE_VERSION=2.3000.1035194821\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;&gt;</span><span class=\"token plain\"> .env</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> --force-recreate evolution-api</span><br></div></code></pre></div></div></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--the-bridge-v1-see-the-webhook\">Step 3 — The bridge, v1: <em>see</em> the webhook<a href=\"https://development-wec.wiline.com/docs/tutorials/whatsapp-ai-assistant-evolution-api/#step-3--the-bridge-v1-see-the-webhook\" class=\"hash-link\" aria-label=\"Direct link to step-3--the-bridge-v1-see-the-webhook\" title=\"Direct link to step-3--the-bridge-v1-see-the-webhook\" translate=\"no\">​</a></h2>\n<p>Now the part you actually write. <strong>Rule: never wire logic before you've seen the real\ndata.</strong> So v1 does nothing but print whatever Evolution sends.</p>\n<p>The bridge runs as another container in the same Compose, so Evolution reaches it <strong>by\nname</strong> (<code>http://bridge:8090</code>) and the bridge reaches Evolution at\n<code>http://evolution-api:8080</code>. Create <code>bridge/main.py</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/evolution-api/bridge/main.py (v1)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastapi </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> FastAPI</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> Request</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">app </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> FastAPI</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@app</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">post</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"/webhook\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">webhook</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">request</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Request</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    data </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> request</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"=== WEBHOOK RECEIVED ===\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">data</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"received\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span><code>flush=True</code> matters</div><div class=\"admonitionContent_BuS1\"><p>Without it, <code>print</code> output is buffered and never shows in <code>docker compose logs</code> — you'll\nstare at an empty log convinced it's broken.</p></div></div>\n<p><code>bridge/Dockerfile</code>:</p>\n<div class=\"language-docker codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/evolution-api/bridge/Dockerfile</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-docker codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FROM python:3.12-slim</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WORKDIR /app</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">RUN pip install --no-cache-dir fastapi uvicorn requests</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">COPY main.py .</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">CMD [\"uvicorn\", \"main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8090\"]</span><br></div></code></pre></div></div>\n<p>Add the service to <code>docker-compose.yml</code> (inside <code>services:</code>), then build it:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.yml (add under services:)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">bridge</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">build</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> ./bridge</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">restart</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> unless</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">stopped</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">env_file</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> .env</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">depends_on</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">evolution</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">api</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">logging</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">driver</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">file</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">options</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">max-size</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"10m\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">max-file</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"3\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--build</span><span class=\"token plain\"> bridge</span><br></div></code></pre></div></div>\n<p>Now <strong>register the webhook</strong> so Evolution POSTs messages to the bridge:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST </span><span class=\"token string\" style=\"color:#e3116c\">\"http://localhost:8080/webhook/set/wec-demo\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"apikey: </span><span class=\"token string variable\" style=\"color:#36acaa\">$APIKEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"webhook\":{\"enabled\":true,\"url\":\"http://bridge:8090/webhook\",\"webhookByEvents\":false,\"events\":[\"MESSAGES_UPSERT\"]}}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'.enabled, .url'</span><br></div></code></pre></div></div>\n<p>Tail the logs (<code>docker compose logs -f bridge</code>) and send yourself a WhatsApp message. The\nreal payload appears:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">A real MESSAGES_UPSERT webhook</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">{'event': 'messages.upsert', 'instance': 'wec-demo',</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> 'data': {'key': {'remoteJid': '123456789012345@lid', 'fromMe': False, 'id': '...'},</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          'pushName': 'Test Contact',</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          'message': {'conversation': 'Oii'},</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          'messageType': 'conversation'},</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> 'sender': '5511988887777@s.whatsapp.net', ...}</span><br></div></code></pre></div></div>\n<p>Three things we now <em>know</em> — and every one drives the code next:</p>\n<ul>\n<li class=\"\"><strong>text</strong> is at <code>data.message.conversation</code> (plain) or <code>data.message.extendedTextMessage.text</code> (quoted)</li>\n<li class=\"\"><strong><code>data.key.fromMe</code></strong> — <code>True</code> for our own messages (the loop guard)</li>\n<li class=\"\"><strong><code>remoteJid</code> ends in <code>@lid</code></strong> — WhatsApp's privacy identifier, <em>not</em> a phone number (this one's a saga; Step 5)</li>\n</ul>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--v2-read-think-reply-with-the-loop-guard\">Step 4 — v2: read, think, reply (with the loop guard)<a href=\"https://development-wec.wiline.com/docs/tutorials/whatsapp-ai-assistant-evolution-api/#step-4--v2-read-think-reply-with-the-loop-guard\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — v2: read, think, reply (with the loop guard)\" title=\"Direct link to Step 4 — v2: read, think, reply (with the loop guard)\" translate=\"no\">​</a></h2>\n<p>Give the bridge the WEC Inference key so it can call the model:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"WEC_API_KEY=&lt;your-wec-inference-key&gt;\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;&gt;</span><span class=\"token plain\"> ~/evolution-api/.env</span><br></div></code></pre></div></div>\n<p>Now the logic — <code>main.py</code> v2:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">~/evolution-api/bridge/main.py (v2)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> requests</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> fastapi </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> FastAPI</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> Request</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">app </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> FastAPI</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">EVOLUTION_URL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://evolution-api:8080\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">INSTANCE </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"wec-demo\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">EVOLUTION_APIKEY </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"AUTHENTICATION_API_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WEC_API_KEY </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"WEC_API_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WEC_URL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"https://inference.wiline.com/v1/chat/completions\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MODEL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Qwen2.5-3B-Instruct\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">ask_llm</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">text</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> requests</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">post</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">WEC_URL</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        headers</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"Bearer </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">WEC_API_KEY</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        json</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"model\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> MODEL</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"messages\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"user\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> text</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        timeout</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">60</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">raise_for_status</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"choices\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"message\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">send_whatsapp</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">number</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> text</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> requests</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">post</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">EVOLUTION_URL</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">/message/sendText/</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">INSTANCE</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        headers</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"apikey\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> EVOLUTION_APIKEY</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"application/json\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        json</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"number\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> number</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"text\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> text</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> timeout</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">60</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"SEND -&gt; </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">r</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">status_code</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@app</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">post</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"/webhook\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">webhook</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">request</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Request</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    data </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token keyword\" style=\"color:#00009f\">await</span><span class=\"token plain\"> request</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"data\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    key </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> data</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"key\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"fromMe\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">                     </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># (1) LOOP GUARD</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"skipped\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"fromMe\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    msg </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> data</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"message\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">             </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># (2) extract text</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    text </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> msg</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"conversation\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> msg</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"extendedTextMessage\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"text\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> text</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"skipped\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"no text\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    to </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"remoteJid\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"IN  </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">to</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">: </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">text</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    answer </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> ask_llm</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">text</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">                     </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># (3) think</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    send_whatsapp</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">to</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> answer</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">                  </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># (4) reply</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"OUT </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">to</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">: </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">answer</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">:</span><span class=\"token string-interpolation interpolation format-spec\">60]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">...\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> flush</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"ok\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">True</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>The one line beginners always miss is <strong>(1) the loop guard</strong>. When the bridge sends a\nreply, that outgoing message fires <em>another</em> <code>MESSAGES_UPSERT</code> webhook with <code>fromMe: true</code>\n— without the guard, the bridge answers itself, forever. Rebuild:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--build</span><span class=\"token plain\"> bridge</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The bridge logging IN and OUT for a live DM\" src=\"https://development-wec.wiline.com/docs/assets/images/wa-bridge-logs-53cb3cf64a3d62d69794ee3a0bf3742b.png\" width=\"768\" height=\"205\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> v2 in action: a DM comes in, the model answers, the reply goes out.</p>\n<p>It answers DMs now. But watch what happens next.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--the-two-real-walls-group-spam-then-lid\">Step 5 — The two real walls: group spam, then LID<a href=\"https://development-wec.wiline.com/docs/tutorials/whatsapp-ai-assistant-evolution-api/#step-5--the-two-real-walls-group-spam-then-lid\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — The two real walls: group spam, then LID\" title=\"Direct link to Step 5 — The two real walls: group spam, then LID\" translate=\"no\">​</a></h2>\n<p>Tail the logs with the bot live and you'll see it try to answer <strong>every group you're in</strong>,\nincluding promo/spam groups, and Evolution <strong>times out</strong> sending to them — flooding errors:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Groups flood the bridge</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">IN  120363040812472138@g.us: *ALÔ CORREDORES 🏃 ... pechin.co/135449*</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">requests.exceptions.ReadTimeout: HTTPConnectionPool(host='evolution-api', port=8080): Read timed out.</span><br></div></code></pre></div></div>\n<p>A companion account receives <strong>everything</strong>. Fix: answer <strong>DMs only</strong> — groups end in\n<code>@g.us</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    to </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"remoteJid\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> to</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">endswith</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"@g.us\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"broadcast\"</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> to</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># DMs only</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"skipped\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"not a DM\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p>Now the subtler wall. With groups filtered, a real DM comes in — but the reply gets\n<strong>rejected</strong>:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">SEND -&gt; 400</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">{\"status\":400,\"error\":\"Bad Request\",\"response\":{\"message\":[</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  {\"exists\":false,\"jid\":\"123456789012345@lid\",\"name\":\"Test Contact\",\"number\":\"123456789012345@lid\"}]}}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Evolution rejecting a reply to an @lid address with exists\" src=\"https://development-wec.wiline.com/docs/assets/images/wa-lid-400-84cc0430abac7bdd5aa6d29cf1c100b5.png\" width=\"863\" height=\"218\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 4.</strong> The LID wall in the logs: the incoming DM arrives as <code>@lid</code>, and the reply is rejected with <code>exists:false</code>.</p>\n<p><code>exists: false</code>. This is <strong>WhatsApp's LID addressing</strong> (a 2025 privacy change): incoming\nDMs arrive with an <code>@lid</code> identifier, and <strong>you cannot send back to an <code>@lid</code></strong> — Evolution\nneeds the real <code>@s.whatsapp.net</code> phone JID. Evolution's contact store has both, though:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST </span><span class=\"token string\" style=\"color:#e3116c\">\"http://localhost:8080/chat/findContacts/wec-demo\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"apikey: </span><span class=\"token string variable\" style=\"color:#36acaa\">$APIKEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{}'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'[.[] | select(.pushName==\"Test Contact\")]'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output — same person, two identities</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[ { \"remoteJid\": \"123456789012345@lid\",         \"pushName\": \"Test Contact\" },</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  { \"remoteJid\": \"5511999999999@s.whatsapp.net\", \"pushName\": \"Test Contact\" } ]</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"findContacts returning the LID and the phone JID for one contact\" src=\"https://development-wec.wiline.com/docs/assets/images/wa-findcontacts-275c01bee94ec0da990f2cdd0ec6acf2.png\" width=\"862\" height=\"429\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 5.</strong> Evolution stores both identities for the same person — the <code>@lid</code> and the real phone JID. That is the key to resolving the reply address.</p>\n<p>So we <strong>resolve the <code>@lid</code> to the phone number</strong> before replying. Add a resolver and use it:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">resolve_number</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">remote_jid</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> push_name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> remote_jid</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">endswith</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"@s.whatsapp.net\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> remote_jid</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">split</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"@\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># LID: find the contact's real number by matching the name</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    contacts </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> requests</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">post</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">EVOLUTION_URL</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">/chat/findContacts/</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">INSTANCE</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        headers</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"apikey\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> EVOLUTION_APIKEY</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"application/json\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        json</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> timeout</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">30</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> c </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> contacts</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> c</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"pushName\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> push_name </span><span class=\"token keyword\" style=\"color:#00009f\">and</span><span class=\"token plain\"> c</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"remoteJid\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">endswith</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"@s.whatsapp.net\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> c</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"remoteJid\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">split</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"@\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><br></div></code></pre></div></div>\n<p>In the handler, resolve before sending:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    number </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> resolve_number</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">to</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> data</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"pushName\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> number</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"skipped\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"unresolved lid\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    send_whatsapp</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">number</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> answer</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Now <code>SEND -&gt; 201</code>. (Matching by name is a heuristic — two contacts sharing a name would\ncollide; production keeps a proper LID→phone map. But it works, and it's the honest state\nof Evolution's LID support in v2.2.3.)</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-6--speak-whatsapp-not-markdown\">Step 6 — Speak WhatsApp, not Markdown<a href=\"https://development-wec.wiline.com/docs/tutorials/whatsapp-ai-assistant-evolution-api/#step-6--speak-whatsapp-not-markdown\" class=\"hash-link\" aria-label=\"Direct link to Step 6 — Speak WhatsApp, not Markdown\" title=\"Direct link to Step 6 — Speak WhatsApp, not Markdown\" translate=\"no\">​</a></h2>\n<p>LLMs emit <strong>Markdown</strong> (<code>**bold**</code>, <code>[text](url)</code>), but WhatsApp has its <em>own</em> formatting:\nbold is <code>*single asterisk*</code>, and Markdown links don't render. So <code>**Compute**</code> shows up\nwith literal asterisks. Translate the model's output before sending, and append the RAG's\nsources as plain URLs:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> re</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">to_whatsapp</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">md</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    md </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> re</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">r\"\\*\\*(.+?)\\*\\*\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">r\"*\\1*\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> md</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">                        </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># **bold** -&gt; *bold*</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    md </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> re</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">r\"\\[([^\\]]+)\\]\\((https?://[^)]+)\\)\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">r\"\\1 (\\2)\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> md</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># [t](url) -&gt; t (url)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    md </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> re</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">r\"(?m)^#{1,6}\\s*\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> md</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">                            </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># drop # headings</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> md</span><br></div></code></pre></div></div>\n<p>Verified against a real answer — exactly what WhatsApp will render:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Formatted output (deterministic, no delivery needed)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">To create a compute instance on WiLine Edge Cloud (WEC), follow these steps:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">1. Log in to the WiLine Edge Cloud.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">2. In the sidebar, click *Compute*.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">3. Select *Instances*.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">4. Click the \"Launch Virtual Machine\" wizard to begin the deployment process.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">_Sources:_</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">https://wec.wiline.com/docs/cloud_portal/platform/compute/instances/compute_instance/</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Markdown-to-WhatsApp formatted answer\" src=\"https://development-wec.wiline.com/docs/assets/images/wa-formatted-output-766cfd620ca72685d8f37110da283d56.png\" width=\"861\" height=\"444\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 6.</strong> The model output after to_whatsapp(): single-asterisk bold, plain links, sources appended.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"level-up--point-the-bridge-at-your-docs-rag\">Level up — point the bridge at your docs (RAG)<a href=\"https://development-wec.wiline.com/docs/tutorials/whatsapp-ai-assistant-evolution-api/#level-up--point-the-bridge-at-your-docs-rag\" class=\"hash-link\" aria-label=\"Direct link to Level up — point the bridge at your docs (RAG)\" title=\"Direct link to Level up — point the bridge at your docs (RAG)\" translate=\"no\">​</a></h2>\n<p>Everything so far uses the raw model, so a WEC question gets a <em>general</em> answer. To make it\nanswer from <strong>your documentation</strong>, swap <code>ask_llm</code> to call the RAG service from the\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/\">evals series</a> — <strong>one function</strong>, nothing\nelse changes:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">RAG_URL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://&lt;your-vm-ip&gt;:8000/ask\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">ask_llm</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">text</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> requests</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">post</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">RAG_URL</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> json</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"question\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> text</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> timeout</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">120</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">raise_for_status</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    d </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    answer </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> to_whatsapp</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">d</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"answer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    sources </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> d</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"sources\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">2</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> sources</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        answer </span><span class=\"token operator\" style=\"color:#393A34\">+=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\\n\\n_Sources:_\\n\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">+</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\\n\"</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">join</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">sources</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> answer</span><br></div></code></pre></div></div>\n<p>That's the payoff of the bridge pattern: <strong>swap the brain, keep the plumbing.</strong> Now WhatsApp\nanswers WEC questions from the real docs, with sources:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"WhatsApp delivering a WEC-docs-grounded answer\" src=\"https://development-wec.wiline.com/docs/assets/images/wa-bridge-reply-bf0eaa9a06721ee1396cbf44fd96ae3a.png\" width=\"1316\" height=\"1729\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 7.</strong> A real message in → the bridge → RAG over the WEC docs → a grounded answer\ndelivered on WhatsApp, sources included.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You self-hosted a programmable WhatsApp gateway and wrote the bridge that turns it into\nan AI assistant — webhook in, model out, reply back — grounded in your own docs.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-delivery-wall\">The delivery wall<a href=\"https://development-wec.wiline.com/docs/tutorials/whatsapp-ai-assistant-evolution-api/#the-delivery-wall\" class=\"hash-link\" aria-label=\"Direct link to The delivery wall\" title=\"Direct link to The delivery wall\" translate=\"no\">​</a></h2>\n<p>Here's the part other tutorials won't tell you. Even with <code>SEND -&gt; 201</code>, replies to a\n<strong>LID-migrated account</strong> frequently arrive as <strong>\"Waiting for this message\"</strong> on the\nrecipient's phone — WhatsApp accepted the ciphertext but no device can decrypt it. It's a\nknown <strong>Baileys companion-device</strong> limitation, and WhatsApp's LID rollout makes it worse.</p>\n<p>What we observed, honestly:</p>\n<ul>\n<li class=\"\">A <strong>fresh re-link</strong> (log out the device, scan a new QR) buys a <strong>short window</strong> where\ndelivery works cleanly.</li>\n<li class=\"\">After a while the session degrades and <code>\"Waiting for this message\"</code> returns — even though\nevery send still reports <code>201</code>.</li>\n</ul>\n<p>So the bridge is correct end-to-end; the <em>unofficial Baileys transport</em> is the weak link.\nIf you're building this for real:</p>\n<ul>\n<li class=\"\"><strong>Use a dedicated, non-LID number</strong> — the issue is tied to LID-migrated accounts.</li>\n<li class=\"\">For production-grade reliability, use the <strong>official WhatsApp Cloud API</strong>, or —\nif your goal is an AI agent rather than a programmable gateway — <strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/\">OpenClaw's native\nWhatsApp channel</a></strong>, which handles the session\nfor you.</li>\n</ul>\n<p>Evolution shines for <strong>outbound automation to numbers you control</strong> (notifications, alerts,\nflows). As a two-way AI assistant on a personal LID account, treat delivery as best-effort.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting-recap\">Troubleshooting recap<a href=\"https://development-wec.wiline.com/docs/tutorials/whatsapp-ai-assistant-evolution-api/#troubleshooting-recap\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting recap\" title=\"Direct link to Troubleshooting recap\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong><code>pull access denied</code> on <code>atendai/evolution-api</code></strong> → image moved to <code>evoapicloud/evolution-api</code>.</li>\n<li class=\"\"><strong>Instance stuck <code>connecting</code>, re-init loop</strong> → stale Baileys version; pin <code>CONFIG_SESSION_PHONE_VERSION</code>.</li>\n<li class=\"\"><strong>Bridge answers itself forever</strong> → missing <code>fromMe</code> loop guard.</li>\n<li class=\"\"><strong>Flood of group messages / send timeouts</strong> → filter <code>@g.us</code>; answer DMs only.</li>\n<li class=\"\"><strong><code>SEND -&gt; 400 exists:false ...@lid</code></strong> → resolve the LID to the phone JID via <code>findContacts</code>.</li>\n<li class=\"\"><strong><code>**asterisks**</code> in replies</strong> → translate Markdown to WhatsApp formatting.</li>\n<li class=\"\"><strong>\"Waiting for this message\"</strong> → Baileys/LID decryption wall; fresh re-link is a temporary\nwindow; use a non-LID number / Cloud API for production.</li>\n</ul>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"teardown\">Teardown<a href=\"https://development-wec.wiline.com/docs/tutorials/whatsapp-ai-assistant-evolution-api/#teardown\" class=\"hash-link\" aria-label=\"Direct link to Teardown\" title=\"Direct link to Teardown\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/evolution-api </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose down          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># add -v to also wipe the DB/session</span><br></div></code></pre></div></div>\n<p>Unlink the device in <strong>WhatsApp → Linked Devices</strong> if you're done.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Self-host a WhatsApp AI bridge</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/whatsapp-ai-assistant-evolution-api/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>You built the reusable bridge pattern — webhook → resolve → model → reply. Point it at a\ndifferent brain, a different channel, or add tools. If you want an AI agent on WhatsApp\nwithout the Baileys caveats, the <strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/\">OpenClaw native WhatsApp\nchannel</a></strong> is the managed path; for docs-grounded\nanswers, the <strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/\">evals &amp; observability series</a></strong>\nbuilds the RAG service this tutorial plugs into.</p>",
            "url": "https://development-wec.wiline.com/docs/tutorials/whatsapp-ai-assistant-evolution-api/",
            "title": "Build a WhatsApp AI assistant from scratch with Evolution and the WEC API",
            "summary": "Self-host a programmable WhatsApp gateway (Evolution API) and write the ~50-line bridge that turns it into an AI assistant — webhook in, LLM out, reply back. Every gotcha is real: the image that moved publishers, the Baileys version loop, the loop guard, group spam, WhatsApp's LID addressing, and the delivery wall nobody warns you about.",
            "date_modified": "2026-07-21T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "self-hosting",
                "whatsapp",
                "evolution-api",
                "docker",
                "inference"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/openclaw-wordmark-003352a1a7f02afc3bd877ae6f2dc175.png\" alt=\"OpenClaw\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/whatsapp_logo-cc34546fcff90d36128c7230c48c496f.png\" alt=\"WhatsApp\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 6 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->6</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->6<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting OpenClaw</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Deploy your own AI assistant</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real HTTPS + auth</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Chat from Telegram</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Private mesh access</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/run-openclaw-on-wec-models/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run it on WEC models</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">🏆</span><span class=\"skillTracker__skill\" data-state=\"current\">Chat from WhatsApp</span></span></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Telegram (<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/\">Part 3</a>) gave your agent a <strong>bot</strong>.\nWhatsApp gives it a <strong>phone line</strong> — the app ~3 billion people already use, reachable with\nzero friction. One catch worth understanding up front: WhatsApp has no bot account, so\nOpenClaw links to a real number as a <strong>companion device</strong> (like WhatsApp Web) and the\nagent acts <em>as that account</em>. Every command and error below is from a real run.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>Continues from Parts 1–4 —\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\">deploy</a>,\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/\">HTTPS</a>,\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/\">Telegram</a>,\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/\">NetBird</a> — on OpenClaw <strong>2026.7.1</strong>. WhatsApp links\nby <strong>QR</strong>, no public webhook or domain needed.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-youll-build\">What you'll build<a href=\"https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/#what-youll-build\" class=\"hash-link\" aria-label=\"Direct link to What you'll build\" title=\"Direct link to What you'll build\" translate=\"no\">​</a></h2>\n<p>Anyone messages the number; the gateway — linked as a companion device — runs it through\nyour model and replies <strong>from the number itself</strong>. No inbound ports, no bot account.</p>\n<!-- -->\n<p><strong>Prerequisites:</strong> Parts 1–4 done (OpenClaw in <code>~/openclaw</code>), a WhatsApp account on your\nphone, ~1 GB free disk. All commands run in <code>~/openclaw</code>.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--add-the-whatsapp-channel\">Step 1 — Add the WhatsApp channel<a href=\"https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/#step-1--add-the-whatsapp-channel\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Add the WhatsApp channel\" title=\"Direct link to Step 1 — Add the WhatsApp channel\" translate=\"no\">​</a></h2>\n<p>OpenClaw treats messaging apps as <em>channels</em>; WhatsApp is a first-class one:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli channels </span><span class=\"token function\" style=\"color:#d73a49\">add</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--channel</span><span class=\"token plain\"> whatsapp</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Installed WhatsApp plugin</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Added WhatsApp account \"default\".</span><br></div></code></pre></div></div>\n<details class=\"details_lb9f alert alert--info details_b_Ee\" data-collapsed=\"true\"><summary><b>Got <code>requires plugin API &gt;=2026.7.1</code>?</b> You're on an older OpenClaw — upgrade first.</summary><div><div class=\"collapsibleContent_i85q\"><p>The WhatsApp channel is a plugin gated to newer runtimes:</p><p><span class=\"zoomImage__wrap\"><img alt=\"The version-wall error on OpenClaw 2026.6.8\" src=\"https://development-wec.wiline.com/docs/assets/images/wa-version-wall-db236509ca6e250b19147327473e0932.png\" width=\"861\" height=\"592\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> The version wall — the WhatsApp plugin needs OpenClaw ≥2026.7.1.</p><p>Back up, pull, recreate, and confirm your existing channels survived (<code>:latest</code> does\n<strong>not</strong> auto-update — you must pull deliberately):</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cp</span><span class=\"token plain\"> ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.bak-</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">date</span><span class=\"token variable\" style=\"color:#36acaa\"> +%Y%m%d</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose pull openclaw-gateway</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--version</span><span class=\"token plain\">          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># expect 2026.7.1+</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> --force-recreate openclaw-gateway</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli channels status    </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Telegram should still be connected</span><br></div></code></pre></div></div><p><span class=\"zoomImage__wrap\"><img alt=\"The pulled image reports 2026.7.1\" src=\"https://development-wec.wiline.com/docs/assets/images/wa-version-check-b1b98be0f2990d46e5686130b2722d3b.png\" width=\"695\" height=\"156\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> After pulling, the image reports 2026.7.1 — the requirement is met.</p><p>Then re-run the <code>channels add</code> above.</p></div></div></details>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--trust-the-plugin\">Step 2 — Trust the plugin<a href=\"https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/#step-2--trust-the-plugin\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Trust the plugin\" title=\"Direct link to Step 2 — Trust the plugin\" translate=\"no\">​</a></h2>\n<p>Check status and the channel is <strong>stopped</strong> with a cryptic error:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"WhatsApp stopped with the openKeyedStore trust error\" src=\"https://development-wec.wiline.com/docs/assets/images/wa-trust-error-7a8033b008450d82190f783b79998f71.png\" width=\"861\" height=\"293\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> <code>stopped</code> — an untrusted plugin can’t use <code>openKeyedStore</code>, so the channel won’t start.</p>\n<p>That's a <strong>security feature</strong>: OpenClaw sandboxes third-party plugins, and an untrusted\none can't use <code>openKeyedStore</code> (the storage WhatsApp needs for its session). Trust it —\ninclude any other non-bundled plugin already loading (here <code>codex</code>) so you don't lock it\nout — and recreate:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli config </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --batch-json </span><span class=\"token string\" style=\"color:#e3116c\">'[{\"path\":\"plugins.allow\",\"value\":[\"codex\",\"whatsapp\"]}]'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> --force-recreate openclaw-gateway</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli channels status</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"plugins.allow now lists codex and whatsapp\" src=\"https://development-wec.wiline.com/docs/assets/images/wa-trust-fix-503842c5815cf95dc4bde07dc3b4a6ee.png\" width=\"673\" height=\"105\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 4.</strong> Trusted — <code>plugins.allow</code> now lists <code>whatsapp</code> (and <code>codex</code>).</p>\n<p>The restart matters — the gateway reads <code>plugins.allow</code> <em>before</em> it initializes the\nplugin. Now it's up:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"WhatsApp running and connected, Telegram still up\" src=\"https://development-wec.wiline.com/docs/assets/images/wa-connected-d4ec6a75486c355d24570487f62305c8.png\" width=\"860\" height=\"294\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 5.</strong> Live — WhatsApp <code>running, connected, healthy</code>, and Telegram still up.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--link-your-phone-by-qr\">Step 3 — Link your phone by QR<a href=\"https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/#step-3--link-your-phone-by-qr\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — Link your phone by QR\" title=\"Direct link to Step 3 — Link your phone by QR\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli channels login</span><br></div></code></pre></div></div>\n<p>Scan the terminal QR from <strong>WhatsApp → Settings → Linked Devices → Link a Device</strong>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The pairing QR, linked after the code 515 restart\" src=\"https://development-wec.wiline.com/docs/assets/images/wa-qr-544d7332e052ce0469c77d7e60fec42a.png\" width=\"786\" height=\"834\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 6.</strong> Scan to link; <code>✅ Linked after restart</code> handles the <code>code 515</code> reconnect.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>The <code>code 515</code> restart is normal</div><div class=\"admonitionContent_BuS1\"><p>WhatsApp forces a reconnect right after pairing; OpenClaw handles it automatically. This\nis exactly where hand-rolled setups get stuck in a loop — here it just works.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--test-it\">Step 4 — Test it<a href=\"https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/#step-4--test-it\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Test it\" title=\"Direct link to Step 4 — Test it\" translate=\"no\">​</a></h2>\n<p>From <strong>another phone</strong>, message the number:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The OpenClaw agent answering on WhatsApp\" src=\"https://development-wec.wiline.com/docs/assets/images/wa-reply-08f4125cb353af3cff9a2418928be7ec.png\" width=\"1320\" height=\"2868\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 7.</strong> A question in, the agent’s answer back — sent from the number, as the account.</p>\n<p>It replies — but notice it answers <strong>from your number, as you.</strong> That's companion mode:\nthe agent <em>is</em> the account. Great for a personal assistant, and the reason for the warning\nbelow.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>Your OpenClaw agent answers on WhatsApp — natively, no bridge, no bot account.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"before-a-real-number\">Before a real number<a href=\"https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/#before-a-real-number\" class=\"hash-link\" aria-label=\"Direct link to Before a real number\" title=\"Direct link to Before a real number\" translate=\"no\">​</a></h2>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>On a personal number the agent replies to everyone, as you</div><div class=\"admonitionContent_BuS1\"><p>It processes <strong>every DM and group</strong> the account receives (<code>Listening for ... DM + all groups</code>). For anything real: link a <strong>dedicated number</strong> (a cheap prepaid SIM — virtual\nnumbers are often rejected), and set an <strong>allowlist</strong> before exposing it.</p></div></div>\n<p>Park the channel when you're done testing (Telegram keeps running; your link stays saved):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli config </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  --batch-json </span><span class=\"token string\" style=\"color:#e3116c\">'[{\"path\":\"channels.whatsapp.accounts.default.enabled\",\"value\":false}]'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> --force-recreate openclaw-gateway</span><br></div></code></pre></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting\">Troubleshooting<a href=\"https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/#troubleshooting\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting\" title=\"Direct link to Troubleshooting\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong><code>openKeyedStore is only available for trusted plugins</code></strong> — channel installs but stays\n<code>stopped</code>. Add the id to <code>plugins.allow</code> and <strong>recreate</strong> the gateway (Step 2). A live\nconfig change isn't enough once it has already failed to start.</li>\n<li class=\"\"><strong>QR loops / <code>code 515</code></strong> — normal post-pairing restart. If it never settles, remove\nstale entries in WhatsApp → Linked Devices and scan a fresh QR.</li>\n<li class=\"\"><strong>It replied to a real contact</strong> — companion mode answers everything; see the warning\nabove and use a dedicated number.</li>\n</ul>\n<hr>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Chat from WhatsApp</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>Part 5 done — your agent reaches Telegram <em>and</em> WhatsApp from a private, hardened box.</p>\n<ul>\n<li class=\"\">Give it <strong>memory that survives restarts</strong> — the <strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/\">Hermes\nAgent</a></strong> series.</li>\n<li class=\"\">Ground it in <em>your</em> docs — the <strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/\">AI evals &amp;\nobservability</a></strong> series builds a RAG\nservice you can point a channel at.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"teardown\">Teardown<a href=\"https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/#teardown\" class=\"hash-link\" aria-label=\"Direct link to Teardown\" title=\"Direct link to Teardown\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli channels remove </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--channel</span><span class=\"token plain\"> whatsapp</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose restart openclaw-gateway</span><br></div></code></pre></div></div>\n<p>And unlink the device in <strong>WhatsApp → Linked Devices</strong> if you're done.</p>",
            "url": "https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/",
            "title": "Add a WhatsApp channel to OpenClaw",
            "summary": "Put your self-hosted agent on WhatsApp. Add the channel, trust the plugin, scan a QR — and understand why a companion link makes the agent act as your account. Every command and error from a real run.",
            "date_modified": "2026-07-20T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "self-hosting",
                "openclaw",
                "whatsapp",
                "chatbot"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__deepeval\" src=\"data:image/png;base64,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\" alt=\"DeepEval\"><span class=\"tutorialHero__plus\">+</span><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 8 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->8</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->8<!-- --> earned</span></div><div class=\"skillTracker__series\">AI evals &amp; observability</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove a model works</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trustworthy JSON</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real test data at scale</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Observe &amp; score production</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">RAG, end to end</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">6</span><span class=\"skillTracker__skill\" data-state=\"current\">Catch regressions in CI</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trace &amp; debug agent tool calls</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/web-search-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Add live web search</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Our <a class=\"\" href=\"https://development-wec.wiline.com/docs/news/gpt-5-6-token-economics/\">GPT-5.6 token-economics analysis</a> ended with a challenge:\nbenchmark promises are a hypothesis, not a reason — run the eval on <em>your</em> workload before you\nbelieve a \"fewer tokens per task\" claim. This tutorial is us taking our own advice.</p>\n<p>We take the RAG docs assistant from <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/\">part 5</a>,\nwrap it in a <strong>DeepEval regression suite</strong> — containerized, judged by <code>gemma4</code> on the WEC\nInference API, zero OpenAI dependency — and then use that suite to answer a genuinely open\nquestion: <strong>should we swap the service's generation model?</strong> Qwen2.5-3B-Instruct (current) vs\ngemma4 (candidate), pass rate and tokens per task, measured head to head.</p>\n<p>Spoiler: the eval saved us from a pointless migration. And along the way we hit four real\nproduction failures — a disk-full death spiral, a startup race condition, a Docker Compose\nvariable trap that silently evaluated the <em>wrong model</em>, and the fix that makes that class of\nbug impossible. Every command, number, and error below is from a real run.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-were-building\">What we're building<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#what-were-building\" class=\"hash-link\" aria-label=\"Direct link to What we're building\" title=\"Direct link to What we're building\" translate=\"no\">​</a></h2>\n<!-- -->\n<p>Three design decisions up front:</p>\n<ol>\n<li class=\"\"><strong>The eval runner is a container</strong>, not a venv. The suite you run locally is byte-for-byte\nthe artifact CI runs — <code>docker compose run eval</code> in both places. (Our first attempt was a\nvenv. It died to a full disk before installing anything; see Troubleshooting.)</li>\n<li class=\"\"><strong>The judge is gemma4 on WEC.</strong> DeepEval assumes OpenAI by default; we give it a 30-line\ncustom model class instead. Questions, answers, and verdicts never leave our infrastructure.</li>\n<li class=\"\"><strong>We reuse part 5's golden dataset unchanged.</strong> The promptfoo <code>tests.csv</code> embeds each\nreference fact inside an <code>llm-rubric</code> string — one regex pulls it back out. Your eval data\noutlives your eval framework.</li>\n</ol>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">The <strong>running RAG service from <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/\">part 5</a></strong>\n(<code>rag-api</code> answering on <code>:8000</code>, <code>eval/tests.csv</code> present).</li>\n<li class=\"\">A <strong>WEC Inference API key</strong> (<a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/inference/api_keys/\">Inference → API Keys</a>).</li>\n<li class=\"\"><strong>~2 GB free disk.</strong> Seriously — check <code>df -h /</code> <em>now</em>. Ours read 100% and the story of why\nis in Troubleshooting.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-0--smoke-test-the-system-under-test\">Step 0 — smoke-test the system under test<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#step-0--smoke-test-the-system-under-test\" class=\"hash-link\" aria-label=\"Direct link to Step 0 — smoke-test the system under test\" title=\"Direct link to Step 0 — smoke-test the system under test\" translate=\"no\">​</a></h2>\n<p>Never eval a service you haven't poked by hand first:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST localhost:8000/ask </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"question\": \"How do I create a compute instance on WEC?\"}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> json.tool</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The RAG service answering a docs question with sources\" src=\"https://development-wec.wiline.com/docs/assets/images/deepeval-smoke-test-98290b8dda1f2a3f5e57a82fa43c2dfa.png\" width=\"900\" height=\"275\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> The system under test, alive: <code>/ask</code> answers from the WEC docs corpus and cites\nits sources. Never eval a service you haven't poked by hand first.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--the-containerized-eval-runner\">Step 1 — the containerized eval runner<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#step-1--the-containerized-eval-runner\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — the containerized eval runner\" title=\"Direct link to Step 1 — the containerized eval runner\" translate=\"no\">​</a></h2>\n<p><code>eval/Dockerfile</code>:</p>\n<div class=\"language-docker codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">eval/Dockerfile</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-docker codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FROM python:3.12-slim</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WORKDIR /eval</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">RUN pip install --no-cache-dir deepeval==3.*</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"># Suite code is mounted at runtime so edits don't need a rebuild</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">CMD [\"deepeval\", \"test\", \"run\", \"test_rag_regression.py\"]</span><br></div></code></pre></div></div>\n<p>Add the service to <code>docker-compose.yml</code> <strong>inside the <code>services:</code> block</strong> (part 5's file ends\nwith a top-level <code>volumes:</code> section — appending blindly puts your service in the wrong block;\n<code>docker compose config --quiet</code> catches it):</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.yml (new service, inside services:)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">eval</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">build</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> ./eval</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">profiles</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"eval\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">env_file</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> .env</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">environment</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">RAG_API_URL</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> http</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">//rag</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">api</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">8000</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">volumes</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> ./eval</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">/eval</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">depends_on</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> rag</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">api</span><br></div></code></pre></div></div>\n<p><code>profiles: [\"eval\"]</code> keeps it out of a normal <code>docker compose up</code> — it only runs when you ask.\nBuild it:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--profile</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> build </span><span class=\"token builtin class-name\">eval</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker compose building the eval runner image\" src=\"https://development-wec.wiline.com/docs/assets/images/deepeval-eval-build-58810296ef048e28edf1a9f90735d2eb.png\" width=\"1619\" height=\"295\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> The eval runner builds in ~42s: <code>python:3.12-slim</code> plus DeepEval 3.x. This exact\nimage is what CI runs later — no venv, no drift.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--a-judge-on-your-own-infrastructure\">Step 2 — a judge on your own infrastructure<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#step-2--a-judge-on-your-own-infrastructure\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — a judge on your own infrastructure\" title=\"Direct link to Step 2 — a judge on your own infrastructure\" translate=\"no\">​</a></h2>\n<p>DeepEval's LLM-as-judge metrics want an OpenAI key. We don't have one and don't want one.\n<code>eval/wec_judge.py</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">eval/wec_judge.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"Custom DeepEval judge backed by WiLine Inference (OpenAI-compatible).\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> openai </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> OpenAI</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> deepeval</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">models</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">base_model </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> DeepEvalBaseLLM</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WEC_BASE_URL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"WEC_BASE_URL\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"https://inference.wiline.com/v1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\">WECJudge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">DeepEvalBaseLLM</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">__init__</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">self</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> model</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:#36acaa\">None</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        self</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">model_name </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> model </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"JUDGE_MODEL\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"gemma4\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        self</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">client </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> OpenAI</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            base_url</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">WEC_BASE_URL</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"WEC_API_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">load_model</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">self</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> self</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">client</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">generate</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">self</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> prompt</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        response </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> self</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">chat</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">completions</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">create</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">self</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">model_name</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            messages</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"user\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> prompt</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            temperature</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> response</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">choices</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">a_generate</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">self</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> prompt</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> self</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">generate</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">prompt</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">get_model_name</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">self</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"WEC/</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">self</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">.</span><span class=\"token string-interpolation interpolation\">model_name</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><br></div></code></pre></div></div>\n<p>Prove it answers from inside the compose network before building anything on it:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--profile</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"from wec_judge import WECJudge; j = WECJudge(); print('judge says:', j.generate('Reply with exactly: WEC judge online'))\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">judge says: WEC judge online</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Judge connectivity check from inside the compose network\" src=\"https://development-wec.wiline.com/docs/assets/images/deepeval-judge-online-c5844beb37c759e19f698f4ea927ec8c.png\" width=\"897\" height=\"170\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> \"WEC judge online\" — gemma4 answering through the container, on the same compose\nnetwork as the service it will judge. No OpenAI key anywhere in the stack.</p>\n<p>An honest caveat we carry through the rest of this tutorial: in the model comparison later,\n<strong>gemma4 is both judge and contestant</strong>. The mitigation is structural — the judge only compares\nanswer-vs-reference-fact, never \"which model wrote this\" — and we watch its verdicts on the\nknown-hard cases for favoritism. (It showed none: every case it failed gemma4 on is a case it also fails Qwen on.)</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can point DeepEval's LLM-judged metrics at any OpenAI-compatible endpoint. Your eval\nverdicts — questions, answers, reasoning — never leave your infrastructure.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--expose-what-you-want-to-measure\">Step 3 — expose what you want to measure<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#step-3--expose-what-you-want-to-measure\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — expose what you want to measure\" title=\"Direct link to Step 3 — expose what you want to measure\" translate=\"no\">​</a></h2>\n<p>Part 5's <code>/ask</code> returned <code>answer</code> and <code>sources</code> — and threw away the token usage that came back\nwith every completion. For token economics we need it, and for the A/B we need the model to be\nswitchable. Three env vars and two response fields in <code>app/main.py</code> (volume-mounted; uvicorn\nhot-reloads, no rebuild):</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">app/main.py (excerpt)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Generation model is configurable so the eval suite can A/B models.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Defaults preserve the tutorial-5 setup.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">GEN_MODEL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"GEN_MODEL\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Qwen2.5-3B-Instruct\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">GEN_BASE_URL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"GEN_BASE_URL\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"https://inference.wiline.com/v1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">GEN_API_KEY </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"GEN_API_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"WEC_API_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p><code>generate()</code> now also returns <code>resp.usage</code>, and <code>/ask</code> reports both:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (abridged)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"answer\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"To view your billing statements ...\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"sources\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"...\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"model\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Qwen2.5-3B-Instruct\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"usage\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"prompt_tokens\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1238</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"completion_tokens\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">122</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"total_tokens\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1360</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"/ask response now reporting model and token usage\" src=\"https://development-wec.wiline.com/docs/assets/images/deepeval-ask-usage-b1a5b036ae1af43fa37d1c91c9d2f092.png\" width=\"1060\" height=\"361\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 4.</strong> The new <code>model</code> and <code>usage</code> fields: 1,238 prompt + 122 completion tokens for one\nbilling question. This is the number the token-economics debate is actually about.</p>\n<p>That <code>usage</code> block is the number the GPT-5.6 marketing is about — measured on your workload,\nper request. Pass the variable through compose (<code>GEN_MODEL: ${GEN_MODEL:-Qwen2.5-3B-Instruct}</code>\nunder <code>rag-api</code>'s <code>environment:</code>) and remember this line — it comes back to bite us in\nTroubleshooting.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--the-regression-suite\">Step 4 — the regression suite<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#step-4--the-regression-suite\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — the regression suite\" title=\"Direct link to Step 4 — the regression suite\" translate=\"no\">​</a></h2>\n<p><code>eval/test_rag_regression.py</code>. It reads part 5's <code>tests.csv</code>, extracts the reference fact from\neach promptfoo rubric, asks the live service, logs token usage, and asserts a GEval correctness\nmetric judged by <code>WECJudge</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">eval/test_rag_regression.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"DeepEval regression suite for the WEC docs RAG service.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"display:inline-block;color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">Reuses the promptfoo golden set (tests.csv) from the previous tutorial:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">the reference fact is parsed out of each llm-rubric string.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> csv</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> re</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> pytest</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> requests</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> deepeval </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> assert_test</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> deepeval</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">metrics </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> GEval</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> deepeval</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">test_case </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> LLMTestCase</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> LLMTestCaseParams</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> wec_judge </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> WECJudge</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">RAG_API_URL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"RAG_API_URL\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://rag-api:8000\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">USAGE_LOG </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"USAGE_LOG\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"usage_log.jsonl\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">MAX_CASES </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">int</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"MAX_CASES\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"0\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 0 = all</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FACT_RE </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> re</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token builtin\">compile</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">r'Reference fact from the official docs: \"(.*?)\"'</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">load_cases</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> </span><span class=\"token builtin\">open</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"tests.csv\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> newline</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> f</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> row </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> csv</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">DictReader</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">f</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            m </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> FACT_RE</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">search</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">row</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"__expected\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">yield</span><span class=\"token plain\"> row</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"question\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">m</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">group</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> m </span><span class=\"token keyword\" style=\"color:#00009f\">else</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">CASES </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">list</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">load_cases</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> MAX_CASES</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    CASES </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> CASES</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">MAX_CASES</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">judge </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> WECJudge</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">correctness </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> GEval</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    name</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"Correctness\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    criteria</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"The actual output must correctly address the input question and must not \"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"contradict the reference fact given as expected output. Different wording \"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"is fine. Fail only if the answer is wrong, contradicts the reference fact, \"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"or fails to answer the question.\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    evaluation_params</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        LLMTestCaseParams</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">INPUT</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        LLMTestCaseParams</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">ACTUAL_OUTPUT</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        LLMTestCaseParams</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">EXPECTED_OUTPUT</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">judge</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    threshold</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">0.5</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@pytest</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">mark</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">parametrize</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:#e3116c\">\"question,reference\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> CASES</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> ids</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">q</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">50</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> q</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> _ </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> CASES</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">test_rag_regression</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">question</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> reference</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> requests</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">post</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">RAG_API_URL</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">/ask\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> json</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"question\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> question</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> timeout</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">120</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">raise_for_status</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    data </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    expect </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"EXPECT_MODEL\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">assert</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">not</span><span class=\"token plain\"> expect </span><span class=\"token keyword\" style=\"color:#00009f\">or</span><span class=\"token plain\"> data</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"model\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> expect</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"suite expected </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">expect</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> but service is running </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">data</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">[</span><span class=\"token string-interpolation interpolation string\" style=\"color:#e3116c\">'model'</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">]</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">with</span><span class=\"token plain\"> </span><span class=\"token builtin\">open</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">USAGE_LOG</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"a\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">as</span><span class=\"token plain\"> f</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        f</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">write</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">dumps</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token string\" style=\"color:#e3116c\">\"model\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> data</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"model\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token string\" style=\"color:#e3116c\">\"question\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> question</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token operator\" style=\"color:#393A34\">**</span><span class=\"token plain\">data</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"usage\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">+</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"\\n\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    assert_test</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        LLMTestCase</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token builtin\">input</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">question</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            actual_output</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">data</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"answer\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            expected_output</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">reference</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">correctness</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>(That <code>EXPECT_MODEL</code> assertion wasn't in the first version. It exists because of a bug you'll\nmeet in a moment.)</p>\n<p>First run, capped at 5 cases to fail fast:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--profile</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">MAX_CASES</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">5</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">DEEPEVAL_TELEMETRY_OPT_OUT</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">YES </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">eval</span><br></div></code></pre></div></div>\n<p><strong>4 of 5 passed in 2:37</strong> — and both the passes and the failure are worth reading. The\nfailure (\"What is the starting point for billing?\", score 0.3) isn't a service bug: the\nanswer was factually fine, but the golden label is a section <em>title</em> (\"How Billing Works\")\nand the judge wanted it named. Remember this case — it flickers between pass and fail all\ntutorial long, and that flicker becomes a finding. Meanwhile on \"total number of events\"\nthe judge scored a 0.6 borderline pass with exactly the right reasoning (the service\ndescribed the feature instead of giving steps). Nuanced verdicts from a self-hosted judge —\nthe thing people assume you need GPT-4-class models for.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"First DeepEval run: 4 of 5 passed, with judge reasoning\" src=\"https://development-wec.wiline.com/docs/assets/images/deepeval-first-run-cabfff276f20b525d0c7c20bb52c1a6a.png\" width=\"1058\" height=\"984\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 5.</strong> First run: 4 of 5 passed in 2:37, every verdict with written reasoning from\nthe gemma4 judge — including the one failure, a strict verdict on a section-title label\nrather than a service bug.</p>\n<p>Note DeepEval's summary line: <code>token cost: None</code>. It can't price a self-hosted judge — which is\nexactly why the suite writes its own <code>usage_log.jsonl</code>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Per-question token log written by the suite\" src=\"https://development-wec.wiline.com/docs/assets/images/deepeval-usage-log-65042f9bd05652b9ff3814cd52b37270.png\" width=\"897\" height=\"575\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 6.</strong> The suite’s own accounting: one JSON line per question — model, prompt, completion,\ntotal — the raw data every comparison in this tutorial is built from.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You have a regression suite grading a live RAG service — reusing last tutorial's golden set\nunchanged, judged by a self-hosted model, with per-question token accounting the framework\nitself doesn't provide.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--the-experiment-qwen25-3b-vs-gemma4\">Step 5 — the experiment: Qwen2.5-3B vs gemma4<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#step-5--the-experiment-qwen25-3b-vs-gemma4\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — the experiment: Qwen2.5-3B vs gemma4\" title=\"Direct link to Step 5 — the experiment: Qwen2.5-3B vs gemma4\" translate=\"no\">​</a></h2>\n<p>The question the suite exists to answer: our service generates with Qwen2.5-3B-Instruct —\nshould it? gemma4 is newer, well-regarded, already in the WEC catalog. Feels like an upgrade.\n<em>Feels.</em> Let's measure.</p>\n<p>Baseline, all 16 cases:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--profile</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">DEEPEVAL_TELEMETRY_OPT_OUT</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">YES </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">USAGE_LOG</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">usage_qwen.jsonl </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">eval</span><br></div></code></pre></div></div>\n<p><strong>14/16 (87.5%) in 8:50.</strong> The two failures are label artifacts, not RAG bugs: the golden facts\nfrom part 5 are section <em>titles</em> (\"How Billing Works\"), and the judge dinged answers that were\nfactually fine but didn't echo the title; and <code>mp3</code> vs the label <code>audio/mpeg</code> — the same fact at\ntwo abstraction levels, ruled a contradiction. Read your failures before trusting your pass\nrate. We leave both in: they're honest label debt, and they become our judge-favoritism canary\nin the A/B.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Qwen baseline: 14 of 16 passed\" src=\"https://development-wec.wiline.com/docs/assets/images/deepeval-qwen-baseline-5b61265199e0d615241bbbfdb30d7a81.png\" width=\"1890\" height=\"977\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 7.</strong> The baseline: 87.5% in 8:50. Both failures are label artifacts, not RAG bugs —\nread your failures before trusting your pass rate.</p>\n<p>Challenger — same suite, one env var (prefix it on <strong>every</strong> compose command; Troubleshooting\nexplains why in blood):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">GEN_MODEL</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">gemma4 </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> rag-api</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">until</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sf</span><span class=\"token plain\"> localhost:8000/health </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> /dev/null</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"waiting...\"</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sleep</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-X</span><span class=\"token plain\"> POST localhost:8000/ask </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"question\": \"ping\"}'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"import json,sys; print('&gt;&gt;&gt; model is:', json.load(sys.stdin)['model'])\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">GEN_MODEL</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">gemma4 </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--profile</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">DEEPEVAL_TELEMETRY_OPT_OUT</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">YES </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">USAGE_LOG</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">usage_gemma4.jsonl </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">EXPECT_MODEL</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">gemma4 </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">eval</span><br></div></code></pre></div></div>\n<p>And verify the log, because run labels lie:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sort</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> eval/usage_gemma4.jsonl </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"import json,sys; [print(json.loads(l)['model']) for l in sys.stdin]\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">uniq</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\">#     16 gemma4</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Switching the service to gemma4 and verifying from the response\" src=\"https://development-wec.wiline.com/docs/assets/images/deepeval-gemma4-run-6a7c64183ec8b68e6d5e5ff76dd0924a.png\" width=\"744\" height=\"196\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 8.</strong> Switching the challenger in: <code>GEN_MODEL=gemma4</code>, wait for <code>/health</code>, then confirm\n<code>&gt;&gt;&gt; model is: gemma4</code> from the response itself — before spending a single eval minute.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"gemma4 run summary: 13 of 16 passed\" src=\"https://development-wec.wiline.com/docs/assets/images/deepeval-gemma4-verified-8d7291e9dfcda2f337f83ca7e4988071.png\" width=\"1890\" height=\"981\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 9.</strong> The gemma4 run: 81.25% in ~13 minutes — and its three failures are the same\nlabel-artifact cases Qwen flickers on. No favoritism from the gemma4 judge toward the gemma4\ncontestant.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-verdict-table\">The verdict table<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#the-verdict-table\" class=\"hash-link\" aria-label=\"Direct link to The verdict table\" title=\"Direct link to The verdict table\" translate=\"no\">​</a></h3>\n<p>One script over the usage logs:</p>\n<table><thead><tr><th>run</th><th>pass rate</th><th>avg prompt tokens</th><th>avg completion tokens</th><th>total tokens</th></tr></thead><tbody><tr><td>Qwen2.5-3B (run 1)</td><td>87.5%</td><td>1,126</td><td><strong>75</strong></td><td>19,225</td></tr><tr><td>Qwen2.5-3B (run 2)</td><td>81.25%</td><td>1,126</td><td><strong>75</strong></td><td>19,225</td></tr><tr><td>gemma4</td><td>81.25%</td><td>1,199</td><td><strong>415</strong></td><td>25,834</td></tr></tbody></table>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Token comparison table: Qwen vs gemma4\" src=\"https://development-wec.wiline.com/docs/assets/images/deepeval-verdict-table-fc9c10d332e08f09d63c768e48c1b23c.png\" width=\"752\" height=\"408\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 10.</strong> The verdict in five lines: same pass rate, 5.5× the completion tokens. The eval\njust prevented a pointless migration.</p>\n<p>Two findings, one expected and one not:</p>\n<p><strong>The verdict: don't switch.</strong> Same effective quality, same failure cases — but gemma4 uses\n<strong>5.5× the completion tokens</strong> (415 vs 75 per answer) and runs ~45% slower per question. On a\nRAG workload where the ~1,100-token retrieved context dominates the prompt, the completion\ncolumn is the whole cost difference. \"Newer model\" lost to \"measure it\" in one afternoon. This\nis precisely the eval the GPT-5.6 news post said to run — and it's the same eval you'd run\nagainst GPT-5.6 itself: change <code>GEN_MODEL</code>, <code>GEN_BASE_URL</code>, and <code>GEN_API_KEY</code>, nothing else.</p>\n<p><strong>The judge has a noise floor.</strong> Look at the two Qwen rows: <strong>identical token counts</strong> —\n19,225 in both runs, because temperature-0 generation is deterministic — yet 87.5% vs 81.25%.\nThe answers didn't change; only the judge's verdicts did (one borderline 0.6 became a 0.3).\nThat cleanly isolates judge nondeterminism from model behavior, and it sets your CI threshold:\na gate at \"≥ 87.5%\" would flake. Ours tolerates one flipped verdict and fails on two or more\nreal regressions.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Qwen run 2: identical answers, different verdicts\" src=\"https://development-wec.wiline.com/docs/assets/images/deepeval-qwen-run2-48156c484cd782d1e346e0938b1080b9.png\" width=\"1055\" height=\"981\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 11.</strong> Qwen run 2: the same 16 deterministic answers, 81.25% this time — the judge, not\nthe model, is where the variance lives.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can settle a model-swap debate with measured data — pass rate <em>and</em> tokens per task — and\nyou know your judge's noise floor, so you can set a CI threshold that won't flake.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-6--the-ci-gate\">Step 6 — the CI gate<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#step-6--the-ci-gate\" class=\"hash-link\" aria-label=\"Direct link to Step 6 — the CI gate\" title=\"Direct link to Step 6 — the CI gate\" translate=\"no\">​</a></h2>\n<p>The payoff of containerizing everything: the CI job is the command you've been running all\nalong. <code>.github/workflows/eval.yml</code>:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">.github/workflows/eval.yml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> rag</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">regression</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">eval</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token key atrule\" style=\"color:#00a4db\">on</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">pull_request</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">paths</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"app/**\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"eval/**\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">workflow_dispatch</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token key atrule\" style=\"color:#00a4db\">jobs</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">eval</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">runs-on</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\">self</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">hosted</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> wec</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># runner with access to inference.wiline.com</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">steps</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">uses</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> actions/checkout@v4</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Run regression suite</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">env</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">          </span><span class=\"token key atrule\" style=\"color:#00a4db\">WEC_API_KEY</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> $</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> secrets.WEC_API_KEY </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">|</span><span class=\"token scalar string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token scalar string\" style=\"color:#e3116c\">          docker compose up -d rag-api</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token scalar string\" style=\"color:#e3116c\">          docker compose --profile eval run --rm \\</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token scalar string\" style=\"color:#e3116c\">            -e DEEPEVAL_TELEMETRY_OPT_OUT=YES \\</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token scalar string\" style=\"color:#e3116c\">            -e EXPECT_MODEL=Qwen2.5-3B-Instruct \\</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token scalar string\" style=\"color:#e3116c\">            eval</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\" style=\"color:#00a4db\">name</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> Teardown</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">if</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> always()</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">run</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> docker compose down</span><br></div></code></pre></div></div>\n<p>Any PR that touches the app or the eval must survive 16 judged questions against the live\nservice — with the model identity asserted — before it merges.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>Your eval is a CI gate. The container you ran locally is byte-for-byte what CI runs — no\n\"works on my machine\" gap between development and the pipeline.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting--the-four-real-failures\">Troubleshooting — the four real failures<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#troubleshooting--the-four-real-failures\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting — the four real failures\" title=\"Direct link to Troubleshooting — the four real failures\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"errno-28-no-space-left-on-device--and-it-came-back\"><code>[Errno 28] No space left on device</code> — and it came back<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#errno-28-no-space-left-on-device--and-it-came-back\" class=\"hash-link\" aria-label=\"Direct link to errno-28-no-space-left-on-device--and-it-came-back\" title=\"Direct link to errno-28-no-space-left-on-device--and-it-came-back\" translate=\"no\">​</a></h3>\n<p>Our first <code>pip install deepeval</code> died: disk 100% full, 58G of 58G. We freed 2 GB of journal\nlogs and build cache… and it was full again <em>minutes later</em>. The culprit:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (the culprit)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">7.9G  /var/lib/docker/containers/10281d.../10281d...-json.log</span><br></div></code></pre></div></div>\n<p><code>langfuse-clickhouse-1</code> was in an error loop, spraying stack traces into a Docker json-log with\n<strong>no rotation configured</strong> — a death spiral, because a full disk causes more errors causes more\nlog. It had been \"unhealthy\" for 8 days. The fix, in order (order matters — truncating while\nthe container runs just refills):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> stop langfuse-clickhouse-1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"truncate -s 0 /var/lib/docker/containers/10281d*/*-json.log\"</span><br></div></code></pre></div></div>\n<p>Then make it permanent — log rotation in the compose override, applied to every service:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.override.yml (Langfuse stack)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">x-logging</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token important\">&amp;default-logging</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">logging</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">driver</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">file</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">options</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">max-size</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"50m\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">max-file</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"3\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token key atrule\" style=\"color:#00a4db\">services</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">clickhouse</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">&lt;&lt;</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token important\">*default-logging</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># ... every other service</span><br></div></code></pre></div></div>\n<p><code>docker compose up -d</code> to recreate. ClickHouse went healthy for the first time in 8 days.\nDocker's json-file driver does <strong>not</strong> rotate by default; on a box running LLM infrastructure\nthat logs enthusiastically, unbounded container logs are a time bomb.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"13-tests-fail-with-connection-refused-then-3-pass\">13 tests fail with <code>Connection refused</code>, then 3 pass<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#13-tests-fail-with-connection-refused-then-3-pass\" class=\"hash-link\" aria-label=\"Direct link to 13-tests-fail-with-connection-refused-then-3-pass\" title=\"Direct link to 13-tests-fail-with-connection-refused-then-3-pass\" translate=\"no\">​</a></h3>\n<p>Changing <code>GEN_MODEL</code> recreates <code>rag-api</code>, and the app takes ~3 minutes to boot (Chroma +\nembedding model load). Our suite started hammering <code>/ask</code> immediately: 13 straight\nconnection-refused failures, then the API came alive mid-run and the last 3 passed. Flaky in\nthe worst way — the failure depends on wall-clock timing.</p>\n<p>Fix it in the suite, not in human discipline — <code>eval/conftest.py</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">eval/conftest.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token triple-quoted-string string\" style=\"color:#e3116c\">\"\"\"Refuse to start the suite until the RAG API is healthy.\"\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> time</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> requests</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">RAG_API_URL </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"RAG_API_URL\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"http://rag-api:8000\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">READY_TIMEOUT </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token builtin\">int</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"READY_TIMEOUT\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"300\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">pytest_configure</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">config</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    deadline </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">time</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">+</span><span class=\"token plain\"> READY_TIMEOUT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">while</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">time</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\"> deadline</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">try</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            requests</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">get</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">RAG_API_URL</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">/health\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> timeout</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">3</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">raise_for_status</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"color:#00009f\">except</span><span class=\"token plain\"> requests</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">RequestException</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            time</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">sleep</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token number\" style=\"color:#36acaa\">3</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">raise</span><span class=\"token plain\"> RuntimeError</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"rag-api not healthy after </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">READY_TIMEOUT</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">s — aborting eval\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-compose-variable-trap-we-evaluated-the-wrong-model-twice\">The compose variable trap: we evaluated the wrong model. Twice.<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#the-compose-variable-trap-we-evaluated-the-wrong-model-twice\" class=\"hash-link\" aria-label=\"Direct link to The compose variable trap: we evaluated the wrong model. Twice.\" title=\"Direct link to The compose variable trap: we evaluated the wrong model. Twice.\" translate=\"no\">​</a></h3>\n<p>The nastiest one, because nothing errored. <code>${GEN_MODEL:-Qwen…}</code> in compose is interpolated\nfrom <em>your shell</em> at <em>every</em> invocation. We started the service with\n<code>GEN_MODEL=gemma4 docker compose up -d</code> — correct. Then ran the eval <strong>without</strong> the prefix.\nCompose resolved the variable back to the Qwen default, saw the running container as config\ndrift, and — via <code>depends_on</code> — <strong>silently recreated <code>rag-api</code> with Qwen</strong>. The run completed,\nthe summary said \"gemma4\" in every verdict (the <em>judge</em> was gemma4), and every answer had been\nwritten by Qwen.</p>\n<p>We only caught it because the suite logs the model <em>from the response</em>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sort</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">..</span><span class=\"token plain\">. usage_gemma4.jsonl </span><span class=\"token punctuation\" style=\"color:#393A34\">..</span><span class=\"token plain\">.</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">uniq</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\">#     16 Qwen2.5-3B-Instruct     ← the \"gemma4\" run</span><br></div></code></pre></div></div>\n<p>Never trust the label on a run; verify from data the system under test reports about itself.\nThe permanent fix is the <code>EXPECT_MODEL</code> assertion in the suite (Step 4). Proof it works — the\ndeliberate-failure test:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">FAILED ... AssertionError: suite expected gemma4 but service is running Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">1 failed in 2.44s</span><br></div></code></pre></div></div>\n<p>Under three seconds, before spending a single judge call — versus the 10+ minutes each mislabeled run\ncost us.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"EXPECT_MODEL guard failing loudly on the wrong model\" src=\"https://development-wec.wiline.com/docs/assets/images/deepeval-expect-model-ccc34be3d936ecfd93823c565073b405.png\" width=\"1058\" height=\"729\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 12.</strong> The guard in action: wrong model → loud failure in under three seconds, before a\nsingle judge call is spent. Compare with the 10+ minutes each silently mislabeled run cost us.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"sleep-5-is-not-a-readiness-check\"><code>sleep 5</code> is not a readiness check<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#sleep-5-is-not-a-readiness-check\" class=\"hash-link\" aria-label=\"Direct link to sleep-5-is-not-a-readiness-check\" title=\"Direct link to sleep-5-is-not-a-readiness-check\" translate=\"no\">​</a></h3>\n<p>Bonus small one: after recreating the container we piped <code>/ask</code> output into a JSON parser 5\nseconds later and got <code>json.decoder.JSONDecodeError: Expecting value: line 1 column 1</code> — the\nserver wasn't up, curl returned empty, the parser choked on nothing. Poll <code>/health</code> in a loop;\nnever sleep a guessed number.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-you-built\">What you built<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#what-you-built\" class=\"hash-link\" aria-label=\"Direct link to What you built\" title=\"Direct link to What you built\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">A <strong>containerized DeepEval regression suite</strong> — same image locally and in CI</li>\n<li class=\"\">A <strong>custom judge on WEC Inference</strong> (<code>gemma4</code> + GEval): verdicts with reasoning, no OpenAI key</li>\n<li class=\"\"><strong>Per-question token accounting</strong> the framework itself couldn't give you</li>\n<li class=\"\">A <strong>measured model decision</strong>: gemma4 ≈ Qwen on quality, 5.5× the completion tokens — keep Qwen</li>\n<li class=\"\">A <strong>judge noise-floor measurement</strong> (deterministic answers, a 6.25-point pass-rate swing) that sets your CI threshold honestly</li>\n<li class=\"\">An <strong><code>EXPECT_MODEL</code> guard</strong> that turns silently-evaluated-the-wrong-model into an under-three-second loud failure</li>\n<li class=\"\">A <strong>GitHub Actions gate</strong> that runs the identical container on every PR</li>\n</ul>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Catch regressions in CI</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>The suite judges <em>answers</em>. Part 4's Langfuse traces know <em>why</em> — which chunks were retrieved,\nwhat the generation span cost, where latency hides. Wiring eval failures back to their traces\n(DeepEval test ID ↔ Langfuse trace ID) turns \"test failed\" into \"here's the retrieval that\ncaused it.\" That's the component-level debugging story, and it's where this series goes next.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://deepeval.com/docs/models-custom\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">DeepEval docs — custom models</a></li>\n<li class=\"\"><a href=\"https://deepeval.com/docs/metrics-llm-evals\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">DeepEval — GEval metric</a></li>\n<li class=\"\"><a href=\"https://docs.docker.com/engine/logging/drivers/json-file/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker logging drivers — json-file rotation</a></li>\n<li class=\"\"><a href=\"https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Compose variable interpolation</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/",
            "title": "Regression-test your RAG service with DeepEval — and settle a model debate with data",
            "summary": "Wrap the RAG assistant from part 5 in a containerized DeepEval suite judged by gemma4 on the WEC Inference API — no OpenAI key anywhere. Then use it to answer a real question: should we swap our generation model? Same quality, 5.5× the tokens: the eval says no. Every command, number, and error is real.",
            "date_modified": "2026-07-20T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "evals",
                "deepeval",
                "rag",
                "docker",
                "ci",
                "inference"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/",
            "content_html": "<div class=\"tutorialHero\"><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-avatar-31b32c657d72dac0a7edbd77c24ec717.jpg\" alt=\"Promptfoo\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2255\" height=\"527\" fill=\"none\" viewBox=\"0 0 2255 527\" class=\"tutorialHero__langfuse\"><path fill=\"#1B1917\" d=\"M652.669 116.433c0-10.261-7.683-17.956-17.926-17.956H607V60h87.923v316.366h-42.254zM804.056 379.786c-39.693 0-72.131-27.362-72.131-68.831 0-41.042 28.596-69.686 84.935-69.686h39.693c7.256 0 12.805-5.558 12.805-12.826v-8.55c0-25.651-20.487-38.905-40.547-38.905-18.353 0-33.291 8.123-41.828 26.934h-45.668c11.95-44.462 46.095-65.41 88.349-65.41 40.547 0 82.374 22.231 82.374 77.808v156.046h-42.68v-14.109c0-5.13-5.549-7.695-9.817-4.702-16.646 11.97-32.438 22.231-55.485 22.231m5.975-38.477c18.78 0 34.572-9.406 52.498-27.362 4.695-4.702 6.829-10.688 6.829-17.1v-6.413c0-7.268-5.549-12.826-12.805-12.826H815.58c-27.743 0-40.974 12.398-40.974 31.637 0 17.956 12.377 32.064 35.425 32.064M961.855 376.366V147.642h42.255v19.666c0 5.13 6.4 6.84 10.24 2.992 13.23-12.825 31.59-27.788 60.61-27.788 36.71 0 70.85 23.086 70.85 75.671v158.183h-42.25V227.161c0-29.499-17.93-45.745-39.7-45.745-20.91 0-35 11.971-49.93 29.499-7.26 8.978-9.82 19.238-9.82 30.354v135.097zM1287.48 467c-52.5 0-85.79-24.796-95.61-63.273h46.1c7.25 15.818 20.06 25.651 45.67 25.651 34.14 0 55.48-20.093 55.48-65.838v-7.268c0-5.13-4.27-7.695-9.39-3.42-14.08 12.398-32.01 20.093-49.08 20.093-58.05 0-96.46-44.889-96.46-115.003 0-70.113 44.39-115.43 98.17-115.43 15.79 0 30.3 4.275 44.38 14.963 5.98 4.275 12.38.855 12.38-5.986v-3.847h42.26V363.54c0 72.678-43.97 103.46-93.9 103.46m-2.56-132.959c19.2 0 33.29-8.55 44.81-20.949 7.26-8.122 9.39-13.68 9.39-26.506v-61.563c0-12.826-2.13-20.948-10.67-29.071-9.39-8.978-22.62-14.964-39.69-14.964-35 0-61.89 29.072-61.89 76.954 0 47.883 24.76 76.099 58.05 76.099M1455.92 199.372c0-7.268-5.97-13.253-13.23-13.253h-32.44v-38.477h32.44c7.26 0 13.23-5.985 13.23-13.253v-5.986c0-45.744 23.48-68.403 69.15-68.403h29.02v38.477h-29.45c-17.5 0-26.46 9.833-26.46 29.926v5.986c0 7.268 5.97 13.253 13.23 13.253h42.68v38.477h-42.68c-7.26 0-13.23 5.985-13.23 13.253v176.994h-42.26zM1652.02 381.496c-35.85 0-69.14-23.086-69.14-75.671V147.642h42.25v150.06c0 29.499 17.07 44.889 37.13 44.889 21.77 0 35.85-11.97 50.79-29.499 7.26-8.977 9.82-19.238 9.82-30.354V147.642h42.25v228.724h-42.25V356.7c0-5.131-6.4-6.841-10.24-2.993-13.24 12.826-31.59 27.789-60.61 27.789M1893.57 381.496c-38.84 0-79.39-19.239-90.06-65.838h43.54c6.4 17.528 23.9 29.498 44.81 29.498 23.05 0 37.13-13.68 37.13-30.353 0-16.246-11.09-25.224-28.59-30.354l-36.28-10.261c-31.58-8.978-55.06-29.499-55.06-64.556 0-38.049 35.43-67.12 75.55-67.12 32.01 0 70.85 14.535 81.09 65.41h-40.55c-5.55-17.528-20.06-29.071-40.54-29.071-20.06 0-34.58 12.398-34.58 28.216 0 13.253 8.11 23.086 27.32 28.644l34.15 9.833c32.43 9.406 58.47 29.072 58.47 66.693 0 39.332-34.15 69.259-76.4 69.259M2098.54 381.496c-61.46 0-102.01-51.73-102.01-119.706s43.11-119.278 101.58-119.278c63.6 0 96.89 51.302 96.89 109.872v23.087h-144.26c-5.98 0-8.54 3.847-7.26 13.68 4.7 32.064 30.31 54.295 55.49 54.295 18.78 0 35-9.405 45.67-27.788h44.81c-16.22 40.187-49.94 65.838-90.91 65.838m43.11-141.51c6.83 0 9.39-3.42 7.68-14.108-4.69-26.506-24.33-45.317-51.22-45.317-25.6 0-47.37 18.811-54.2 45.745-2.56 9.833.85 13.68 6.83 13.68z\"></path><path fill=\"#FF5D5F\" d=\"m286.292 286.105 34.597 27.791s26.473-19.661 45.941-22.545c20.418-3.025 42.202 8.359 62.388 21.93 30.489 20.498 56.149 46.508 56.149 46.508l30.06-29.493s-82.879-89.795-148.597-81.672c-43.105 5.328-80.538 37.481-80.538 37.481\"></path><path fill=\"#4E9CFF\" d=\"M88.358 114.862 60 146.056s79.009 73.732 141.224 73.732c28.358 0 67.684-22.216 101.523-51.079 19.283-16.448 40.835-35.13 62.388-35.13 14.487 0 33.594 7.673 51.612 27.824 0 0 11.63-6.974 18.716-11.985 6.228-4.404 15.479-11.91 15.479-11.91-25.918-27.663-63.407-47.883-85.807-45.9-36.299.005-62.388 22.601-94.717 48.735s-45.94 36.907-69.194 36.907c-39.134 0-112.866-62.388-112.866-62.388M88.358 352.463 60 321.269s79.009-73.732 141.224-73.732c28.358 0 67.684 22.216 101.523 51.079 19.283 16.448 40.835 35.13 62.388 35.13 14.556 0 33.518-7.989 51.612-28.358 0 0 10.877 6.705 17.582 11.344 6.894 4.769 17.015 12.655 17.015 12.655-25.931 27.883-63.693 48.323-86.209 46.33-36.299-.005-57.851-19.24-90.179-45.374-32.329-26.133-50.478-40.268-73.732-40.268-39.134 0-112.866 62.388-112.866 62.388M458.142 185.149c-7.378 5.1-19.283 12.478-19.283 12.478s6.806 14.746 6.806 34.597-6.239 36.866-6.239 36.866 10.688 6.675 17.582 11.343c7.162 4.849 18.149 13.045 18.149 13.045s13.045-27.224 13.045-61.254-13.045-59.552-13.045-59.552-10.236 7.792-17.015 12.477\"></path><path fill=\"#FF5D5F\" d=\"m287.995 180.612 32.895-27.224s26.473 19.046 45.941 21.93c20.417 3.026 42.202-8.359 62.388-21.93 30.489-20.498 56.149-46.507 56.149-46.507l30.06 29.492s-82.879 89.795-148.597 81.672c-43.105-5.328-78.836-37.433-78.836-37.433M208.601 91c42.538 0 78.264 36.299 78.264 36.299s-9.941 7.832-16.448 13.045c-6.777 5.429-17.582 14.179-17.582 14.179s-18.711-19.851-44.234-19.851c-10.465 0-24.066 6.286-38.567 18.716-11.188 9.591-22.829 21.514-30.627 36.299-6.743 12.784-10.42 27.85-10.776 43.672-.446 19.873 6.597 40.704 18.149 57.283 7.743 11.112 16.983 19.474 26.657 26.657 12.555 9.322 25.648 15.881 35.164 15.881 10.166 0 19.306-3.533 26.09-6.806 10.776-6.239 19.278-13.612 19.278-13.612l33.463 27.791s-13.612 13.612-32.323 23.821c-12.091 5.963-27.632 11.91-46.508 11.91-18.862 0-40.767-10.022-61.254-25.522-13.244-10.021-26.225-21.895-36.298-36.299-16.51-23.607-25.017-52.328-24.96-81.104.057-29.136 9.451-57.993 26.094-81.672C138.273 117.657 176.86 91 208.601 91\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 8 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->8</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->8<!-- --> earned</span></div><div class=\"skillTracker__series\">AI evals &amp; observability</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove a model works</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trustworthy JSON</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real test data at scale</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Observe &amp; score production</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">5</span><span class=\"skillTracker__skill\" data-state=\"current\">RAG, end to end</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">Catch regressions in CI</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trace &amp; debug agent tool calls</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/web-search-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Add live web search</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Everything in this series was building to this. You can prove a model works\n(<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\">part 1</a>), make its output machine-reliable\n(<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/\">part 2</a>), generate real test data\n(<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/\">part 3</a>), and observe production\n(<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/\">part 4</a>). Now we spend all four skills at once on\nthe pattern behind almost every serious LLM product: <strong>RAG</strong> — retrieval-augmented generation.</p>\n<p>We'll build a <strong>docs assistant</strong>: a containerized HTTP service that answers customer questions\nfrom WEC's own documentation. Not a notebook — a service, Docker-first, the shape you'd actually\ndeploy. And because this series doesn't do happy-path demos: along the way our RAG <strong>hallucinates a\nGPU price</strong>, we root-cause it to <strong>our own scraper</strong>, fix it, and pin the fix with a regression\ntest. Every command, number, and error below is from a real run.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-were-building\">What we're building<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#what-were-building\" class=\"hash-link\" aria-label=\"Direct link to What we're building\" title=\"Direct link to What we're building\" translate=\"no\">​</a></h2>\n<!-- -->\n<p>One architecture decision up front: we split the work. <strong>Embeddings run locally</strong> (a small ONNX\nmodel — fast, free, no GPU needed) and <strong>generation runs on WEC</strong>. This is a completely standard\nproduction split: retrieval is cheap and latency-sensitive, so keeping it next to your vector\nstore saves a network round-trip per query — while generation is where the big model earns its\nkeep.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Prefer managed embeddings? WEC has them now</div><div class=\"admonitionContent_BuS1\"><p>The WEC catalog now ships <strong><code>bge-m3</code></strong> (1024-dim, multilingual, batch input) on\n<code>/v1/embeddings</code>:</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> https://inference.wiline.com/v1/embeddings </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$WEC_API_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"bge-m3\",\"input\":[\"first chunk\",\"second chunk\"]}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'.data | length'</span><br></div></code></pre></div></div><p>Swapping this tutorial over is a two-function change — replace the <code>fastembed</code> calls in\n<code>ingest.py</code> and <code>main.py</code> with the same OpenAI client pointed at <code>/v1/embeddings</code>\n(<code>wec.embeddings.create(model=\"bge-m3\", input=chunks)</code>), and re-ingest (the vector dimension\nchanges from 384 to 1024, so drop and rebuild the collection). We keep the local path below\nbecause it's free, offline, and every number in this guide was measured with it — but both are\nproduction-legitimate; pick per your latency and ops preferences.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>Docker + Compose</strong> on a WEC Instance (the same setup as\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/\">part 4</a>'s Langfuse box works).</li>\n<li class=\"\">A <strong>WEC Inference API key</strong> (<a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/inference/api_keys/\">Inference → API Keys</a>).</li>\n<li class=\"\">Your part-4 <strong>Langfuse</strong> instance and keys (optional but recommended — used in Step 3's tracing).</li>\n<li class=\"\"><strong>~1 GB free disk</strong> for the image. Check first: <code>df -h /</code>. Our box was at 99% and survived, but\ntight disk is the #1 silent killer of Docker builds.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--scaffold-the-service-docker-first\">Step 1 — Scaffold the service, Docker-first<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#step-1--scaffold-the-service-docker-first\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Scaffold the service, Docker-first\" title=\"Direct link to Step 1 — Scaffold the service, Docker-first\" translate=\"no\">​</a></h2>\n<p>Real RAG features ship as services, so we start as one: a FastAPI app in a container, code\nbind-mounted so <code>uvicorn --reload</code> picks up every edit — no rebuild per change.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> ~/rag-service/app </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/rag-service</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> requirements.txt </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">fastapi==0.115.6</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">uvicorn==0.34.0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">chromadb==0.5.23</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">fastembed==0.4.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">openai==1.59.7</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">langfuse</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">requests==2.32.3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">beautifulsoup4==4.12.3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Dockerfile </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">FROM python:3.12-slim</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">WORKDIR /srv</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">COPY requirements.txt .</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">RUN pip install --no-cache-dir -r requirements.txt</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">COPY app/ app/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EXPOSE 8000</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">CMD [\"uvicorn\", \"app.main:app\", \"--host\", \"0.0.0.0\", \"--port\", \"8000\", \"--reload\"]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> docker-compose.yml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">services:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  rag-api:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    build: .</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    ports:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - \"8000:8000\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    volumes:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - ./app:/srv/app          # live code - edit on host, uvicorn reloads</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - chroma-data:/data       # persisted vector index</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - model-cache:/root/.cache # embedding model cache (survives rebuilds)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    environment:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      ANONYMIZED_TELEMETRY: \"False\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      WEC_API_KEY: ${WEC_API_KEY}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      LANGFUSE_PUBLIC_KEY: ${LANGFUSE_PUBLIC_KEY}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      LANGFUSE_SECRET_KEY: ${LANGFUSE_SECRET_KEY}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      LANGFUSE_HOST: ${LANGFUSE_HOST}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">volumes:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  chroma-data:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  model-cache:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div></code></pre></div></div>\n<p>Secrets live in an <code>.env</code> file next to the compose (compose loads it automatically — keys never\nenter the YAML). Keep it out of version control from the start:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">WEC_API_KEY=sk-your-wec-key</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">LANGFUSE_PUBLIC_KEY=pk-lf-…</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">LANGFUSE_SECRET_KEY=sk-lf-…</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">LANGFUSE_HOST=http://&lt;your-langfuse-host&gt;:3000</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\".env\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;&gt;</span><span class=\"token plain\"> .gitignore</span><br></div></code></pre></div></div>\n<p>A minimal app so the container has something to serve:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> app/main.py </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from fastapi import FastAPI</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">app = FastAPI(title=\"WEC docs RAG\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">@app.get(\"/health\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">def health():</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    return {\"status\": \"ok\"}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--build</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> localhost:8000/health</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">{\"status\":\"ok\"}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The containerized service alive — compose ps + health check\" src=\"https://development-wec.wiline.com/docs/assets/images/rag-deploy-health-850c74e1ed3eff7a20368db371456f79.png\" width=\"948\" height=\"117\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> One command each: the container up, <code>/health</code> answering, and the disk check\n(ours reads 99% — this box lives dangerously). The build takes ~90s and a ~700 MB image.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--ingest-build-the-corpus-from-a-live-website\">Step 2 — Ingest: build the corpus from a live website<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#step-2--ingest-build-the-corpus-from-a-live-website\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Ingest: build the corpus from a live website\" title=\"Direct link to Step 2 — Ingest: build the corpus from a live website\" translate=\"no\">​</a></h2>\n<p>In the real world, the knowledge base you'll want your assistant to answer from usually isn't a\ntidy folder of markdown — it's a <strong>live website</strong>: product docs, a wiki, a help center. So the\ncorpus-acquisition skill worth learning is real scraping, not file loading.</p>\n<p>For this tutorial we scrape <strong>WEC's own documentation site</strong>. It's built with Docusaurus, and\nDocusaurus (like most site generators) publishes a <code>sitemap.xml</code> — which means you can discover\nevery page programmatically instead of guessing URLs. The same approach works on any site that\nships a sitemap:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> https://wec.wiline.com/docs/sitemap.xml </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"&lt;loc&gt;\"</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 109 URLs on the whole site</span><br></div></code></pre></div></div>\n<p>We filter to the product docs (<code>/docs/cloud_portal/</code>), pull each page, and keep only the\n<code>&lt;article&gt;</code> element — Docusaurus wraps the actual content in it, so nav/sidebar/footer never reach\nthe index:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> app/ingest.py </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"\"\"Scrape the WEC docs site -&gt; chunk -&gt; embed locally -&gt; Chroma.\"\"\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">import re, requests, chromadb</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from bs4 import BeautifulSoup</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from fastembed import TextEmbedding</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">SITEMAP = \"https://wec.wiline.com/docs/sitemap.xml\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">FILTER  = \"/docs/cloud_portal/\"          # product docs only</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">CHUNK, OVERLAP = 700, 100                # chars</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">def discover():</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    xml = requests.get(SITEMAP, timeout=30).text</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    urls = re.findall(r\"&lt;loc&gt;([^&lt;]+)&lt;/loc&gt;\", xml)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    return [u for u in urls if FILTER in u]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">def scrape(url):</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    html = requests.get(url, timeout=30).text</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    art = BeautifulSoup(html, \"html.parser\").find(\"article\")  # Docusaurus main content</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    return art.get_text(\"\\n\", strip=True) if art else \"\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">def chunk(text):</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    out, i = [], 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    while i &lt; len(text):</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        out.append(text[i:i + CHUNK])</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        i += CHUNK - OVERLAP</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    return out</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">if __name__ == \"__main__\":</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    urls = discover()</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print(f\"sitemap -&gt; {len(urls)} product-doc pages\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    docs, metas = [], []</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    for u in urls:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        for c in chunk(scrape(u)):</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            docs.append(c)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            metas.append({\"url\": u})</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print(f\"scraped -&gt; {len(docs)} chunks\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    embedder = TextEmbedding(\"BAAI/bge-small-en-v1.5\")   # local, ~66MB ONNX, no GPU needed</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    vectors = [v.tolist() for v in embedder.embed(docs)]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print(f\"embedded -&gt; {len(vectors)} vectors (dim {len(vectors[0])})\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    db = chromadb.PersistentClient(path=\"/data\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    col = db.get_or_create_collection(\"wec-docs\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    col.add(ids=[str(i) for i in range(len(docs))],</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            documents=docs, metadatas=metas, embeddings=vectors)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    print(f\"indexed -&gt; collection 'wec-docs' now has {col.count()} chunks\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> rag-api python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> app.ingest</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sitemap -&gt; 37 product-doc pages</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">scraped -&gt; 281 chunks</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">model_optimized.onnx: 100%|████████████| 66.5M/66.5M [00:03&lt;00:00, 16.9MB/s]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">embedded -&gt; 281 vectors (dim 384)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">indexed -&gt; collection 'wec-docs' now has 281 chunks</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Ingest run — 37 pages, 281 chunks, embedded and indexed\" src=\"https://development-wec.wiline.com/docs/assets/images/rag-ingest-2a1ffde1c98f1968bbf60532fa0a0a00.png\" width=\"941\" height=\"357\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> The whole product-doc corpus indexed in under a minute. The embedding model caches in\nthe <code>model-cache</code> volume, so it downloads exactly once.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can turn any documentation website into a queryable vector index — sitemap discovery, content\nextraction, chunking, local embeddings.</p></div></div>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>About <code>get_text(\"\\n\", …)</code></div><div class=\"admonitionContent_BuS1\"><p>Our first version used <code>get_text(\" \")</code> — a single space as the separator. It looked harmless and\n<strong>caused a genuine hallucination</strong> you'll see in Step 4. Keep the newline; we'll come back to this.\n(Want to watch the bug happen on your own box? Change <code>\"\\n\"</code> back to <code>\" \"</code> in <code>scrape()</code>, re-run\nthe ingest, and ask the Step-4 question — then put the newline back and re-ingest.)</p></div></div>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Chroma telemetry noise</div><div class=\"admonitionContent_BuS1\"><p>You may see <code>Failed to send telemetry event ClientStartEvent: capture() takes 1 positional argument…</code> — a known chromadb/posthog version clash. It's harmless (your data is fine), and the\n<code>ANONYMIZED_TELEMETRY: \"False\"</code> env in the compose silences it.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--the-ask-endpoint-retrieve-generate-trace\">Step 3 — The <code>/ask</code> endpoint: retrieve, generate, trace<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#step-3--the-ask-endpoint-retrieve-generate-trace\" class=\"hash-link\" aria-label=\"Direct link to step-3--the-ask-endpoint-retrieve-generate-trace\" title=\"Direct link to step-3--the-ask-endpoint-retrieve-generate-trace\" translate=\"no\">​</a></h2>\n<p>Now the service itself. Three design choices worth stating:</p>\n<ul>\n<li class=\"\"><strong>Citations come from retrieval metadata, not the model.</strong> The model can hallucinate URLs; the\nvector store cannot. <code>sources</code> is deterministic.</li>\n<li class=\"\"><strong>The WEC call goes through <code>langfuse.openai</code></strong> (part 4's drop-in), and each stage gets an\n<code>@observe</code> span — so every request produces a <strong><code>ask → retrieve → generate</code> tree</strong> in Langfuse.</li>\n<li class=\"\"><strong><code>timeout=60</code></strong> on the client. Part 4's lesson: no default timeout means a stalled backend hangs\nyour service silently.</li>\n</ul>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> app/main.py </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">import os, chromadb</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from fastapi import FastAPI</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from pydantic import BaseModel</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from fastembed import TextEmbedding</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from langfuse import observe, get_client</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from langfuse.openai import openai          # auto-traces the WEC call</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">app = FastAPI(title=\"WEC docs RAG\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">db = chromadb.PersistentClient(path=\"/data\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">col = db.get_or_create_collection(\"wec-docs\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">embedder = TextEmbedding(\"BAAI/bge-small-en-v1.5\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">wec = openai.OpenAI(</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    base_url=\"https://inference.wiline.com/v1\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    api_key=os.environ[\"WEC_API_KEY\"],</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    timeout=60,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">class Ask(BaseModel):</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    question: str</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">@observe()                                   # span: retrieve</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">def retrieve(question: str, k: int = 4):</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    qv = list(embedder.embed([question]))[0].tolist()</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    res = col.query(query_embeddings=[qv], n_results=k)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    chunks = res[\"documents\"][0]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    sources = sorted({m[\"url\"] for m in res[\"metadatas\"][0]})</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    return chunks, sources</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">@observe()                                   # span: generate (WEC call nested inside)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">def generate(question: str, chunks: list[str]) -&gt; str:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    context = \"\\n---\\n\".join(chunks)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    resp = wec.chat.completions.create(</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        model=\"Qwen2.5-3B-Instruct\",     # small on purpose: cheap + fast per query,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">                                         # and its failures teach (see Step 5)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        name=\"rag-generate\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        messages=[{\"role\": \"user\", \"content\":</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            \"Answer the question using ONLY the context below. \"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            \"If the context doesn't contain the answer, say so. \"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            \"When quoting a price or number, copy it VERBATIM with its unit and the item \"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            \"it belongs to; never combine numbers from different lines and never compute \"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            \"new numbers from examples.\\n\\n\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            f\"Context:\\n{context}\\n\\nQuestion: {question}\"}],</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    )</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    return resp.choices[0].message.content</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">@app.post(\"/ask\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">@observe()                                   # root span: the whole request</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">def ask(body: Ask):</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    chunks, sources = retrieve(body.question)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    answer = generate(body.question, chunks)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    get_client().flush()</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    return {\"answer\": answer, \"sources\": sources}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">@app.get(\"/health\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">def health():</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    return {\"status\": \"ok\"}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--build</span><br></div></code></pre></div></div>\n<p>Ask it a real customer question:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> localhost:8000/ask </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"question\": \"How do I create an API key for the Inference API?\"}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> json.tool</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (abridged)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"answer\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"To create an API key for the Inference API, follow these steps:\\n1. Log in to the WiLine Edge Cloud.\\n2. Expand the \\\"Inference\\\" section in the left sidebar.\\n3. Click on the \\\"API Keys\\\" tab.\\n4. Click the \\\"+ Create Key\\\" button located above the keys table. ...\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token property\" style=\"color:#36acaa\">\"sources\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"https://wec.wiline.com/docs/cloud_portal/platform/inference/api/wiline-edge-cloud-inference-api/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"https://wec.wiline.com/docs/cloud_portal/platform/inference/api_keys/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"https://wec.wiline.com/docs/cloud_portal/platform/inference/examples/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"https://wec.wiline.com/docs/cloud_portal/platform/inference/models_hub/\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:#e3116c\">\"https://wec.wiline.com/docs/cloud_portal/platform/inference/overview/\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"First real answer — correct steps, correct sources\" src=\"https://development-wec.wiline.com/docs/assets/images/rag-first-ask-301b6dfc7a64a463e0eb1e6d93b53837.png\" width=\"834\" height=\"308\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> First try: the exact API-key flow from the docs, with the right page cited. Scrape →\nembed → retrieve → generate, end to end.</p>\n<p>Open Langfuse → Tracing and you'll find the request as a <strong>span tree</strong> — <code>ask → retrieve → generate → rag-generate</code> — with per-step latency and tokens. When an answer is bad, this is how you\ntell <em>bad retrieval</em> from <em>bad generation</em> (part 4's skill, now on a real app).</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The RAG request as a span tree in Langfuse\" src=\"https://development-wec.wiline.com/docs/assets/images/rag-trace-tree-769e5224814989dedc4896f9e4e1873f.png\" width=\"1904\" height=\"996\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 4.</strong> Every <code>/ask</code> becomes a tree: retrieval and generation each have an address.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You have a traced, containerized RAG service answering from live-scraped documentation.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--the-hallucination-we-caught-for-real\">Step 4 — The hallucination we caught (for real)<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#step-4--the-hallucination-we-caught-for-real\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — The hallucination we caught (for real)\" title=\"Direct link to Step 4 — The hallucination we caught (for real)\" translate=\"no\">​</a></h2>\n<p>A good RAG must refuse what the docs don't cover. Probe it:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> localhost:8000/ask </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"question\": \"Does WEC offer a free GPU tier for students?\"}'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> json.tool</span><br></div></code></pre></div></div>\n<p>Our first version (the <code>get_text(\" \")</code> scraper) answered:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (first version — spot the bug)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">\"No, WEC does not offer a free GPU tier specifically for students. The pricing for</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">GPU hourly is listed as $0.66/GPU-hr with a 25% off annual prepay option at</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">$0.10/GB-mo for block storage. There is no mention of a free GPU tier in the</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">provided information.\"</span><br></div></code></pre></div></div>\n<p>The refusal is right. But read the pricing sentence again: <em>\"$0.66/GPU-hr <strong>with</strong> a 25% off annual\nprepay option <strong>at $0.10/GB-mo</strong> for block storage\"</em> — that's three different facts welded into one\nfalse statement, gluing a storage rate onto the GPU prepay option. Check the actual pricing page\nand you find they're simply three adjacent items:</p>\n<ul>\n<li class=\"\">GPU hourly: <strong>$0.66/GPU-hr</strong></li>\n<li class=\"\">Annual prepay: <strong>25% off</strong></li>\n<li class=\"\">Block storage: <strong>$0.10/GB-mo</strong></li>\n</ul>\n<p>The model stitched a storage price onto \"GPU\". Why? Look at the chunk we fed it. Our\nspace-separated scrape had flattened the pricing <em>cards</em> into one undifferentiated line:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">The chunk we actually embedded (first version)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">... $1.44 /mo Starting at $0.66 /GPU-hr GPU hourly 25 % off Annual prepay $0.10 /GB-mo Block storage ...</span><br></div></code></pre></div></div>\n<p><strong>The hallucination was manufactured by our own ingestion.</strong> The model conflated prices because we\ndestroyed the structure before embedding it. This is the most classic RAG failure there is —\ngarbage in, garbage out — and you only catch it by probing with questions and <em>reading the\nretrieved chunks</em>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The subtle hallucination — three price cards welded into one claim\" src=\"https://development-wec.wiline.com/docs/assets/images/rag-hallucination-18e9fb70cb217a347f31057d36b774e8.png\" width=\"947\" height=\"525\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 5.</strong> The bug in the wild: correct refusal, garbled pricing — a storage rate glued onto the\nGPU prepay option. The sources look perfectly plausible. This is why \"it cites sources\" is not the\nsame as \"it's grounded.\"</p>\n<p>Two fixes, both already in the code above:</p>\n<ol>\n<li class=\"\"><strong>Structure-aware extraction</strong> — <code>get_text(\"\\n\", strip=True)</code> keeps line boundaries, so price\ncards stay separate lines instead of one soup.</li>\n<li class=\"\"><strong>A hardened generation prompt</strong> — quote numbers verbatim with unit and item; never combine\nlines; never derive new numbers from examples. (Before this rule, the model happily <em>computed</em>\na fake \"$1.5834/GPU-hour\" from a daily-spend example.)</li>\n</ol>\n<p>After re-ingesting with the fix, the storage-price conflation is gone. Residual ambiguity remains —\nthe pricing strip still contains fragments like \"$1.44/mo\" (a compute-instance price) near the GPU\nline, and truly fixing that needs table-aware extraction (see <em>What's next</em>). Which is exactly why\nthe next step exists: <strong>pin the fixed bug so it can never quietly return.</strong></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--eval-the-service-like-a-ci-gate\">Step 5 — Eval the service like a CI gate<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#step-5--eval-the-service-like-a-ci-gate\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — Eval the service like a CI gate\" title=\"Direct link to Step 5 — Eval the service like a CI gate\" translate=\"no\">​</a></h2>\n<p>This step took <strong>four rounds of fixes to get an honest number</strong> — and the journey from 39% to 87%\nis the best lesson in the series, because almost nothing we fixed along the way was the RAG itself.</p>\n<p>We reuse part 3's skill — generate a QA dataset <em>from the corpus itself</em> — and grade the <strong>live\nHTTP endpoint</strong> with Promptfoo. The first generator version asked for one question per page with a\n<code>key_fact</code> the answer must contain, and got 37/37 pairs back. Some labels already smelled — a vague\n<code>\"All paid\"</code>, menu paths instead of answers — but let's run it naively first and let the failures\nteach us. Convert to a Promptfoo dataset with plain substring assertions (<code>icontains:</code> + the key\nfact) and point it at the running service. The provider is <strong>HTTP</strong> — we grade the real API,\nexactly what a CI gate would do:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'question,__expected'</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'[.question, (\"icontains:\" + .key_fact)] | @csv'</span><span class=\"token plain\"> app/qa.jsonl</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> eval/tests.csv</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> eval/promptfooconfig.yaml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">description: \"RAG service eval — QA set generated from the scraped docs + regression\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">providers:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: https</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      url: http://localhost:8000/ask</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      method: POST</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      headers:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        Content-Type: application/json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      body:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        question: \"{{question}}\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      transformResponse: json.answer</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">tests:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - file://tests.csv</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  # regression: the storage-price-as-GPU-price conflation from Step 4</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      question: \"What is the pricing model for GPU hourly usage in Edge Cloud?\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - type: not-icontains</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        value: \"0.10\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - type: not-icontains</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        value: \"GB-mo\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml --no-cache</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (development run — the service still had the Step-4 bug)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Results:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ✓ 15 passed (39.47%)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ✗ 23 failed (60.53%)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Duration: 40s (concurrency: 4)</span><br></div></code></pre></div></div>\n<p>That 39.5% is what we measured <em>during development</em>, while the Step-4 bug was still live. Re-run\nthe same naive eval against the <strong>finished</strong> service and it lands much higher — 27/38:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The naive eval against the finished service — 71%\" src=\"https://development-wec.wiline.com/docs/assets/images/rag-eval-icontains-284e4ea4fa5a8f2dec99a5079c84c4f7.png\" width=\"827\" height=\"973\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 6.</strong> The same naive eval against the finished service: <strong>71.05%</strong>. Better service, same\nnoisy labels — the 30-point gap between these two runs is all service quality, and both numbers\nmean little until you read the rows.</p>\n<p><strong>Read the failures before you panic.</strong> From the development run's 23 failures, most are not RAG\nfailures:</p>\n<table><thead><tr><th>Failure</th><th>What actually happened</th></tr></thead><tbody><tr><td>\"Balance due\" answer: <em>\"the amount that remains due… $0.00\"</em></td><td>Correct answer; the label demanded the phrase <code>All paid</code> — <strong>bad label</strong></td></tr><tr><td>Payment Information: gives the exact right menu path</td><td>Correct; label phrasing doesn't substring-match — <strong>brittle assertion</strong></td></tr><tr><td>GPU pricing: <em>\"$0.10 per GB-month\"</em></td><td><strong>Real failure</strong> — the Step-4 conflation, reproducing systematically</td></tr><tr><td>TTS default format: <em>\"not specified in the context\"</em></td><td><strong>Real retrieval miss</strong> — the chunk with the default wasn't retrieved</td></tr></tbody></table>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"upgrade-1--a-judge-instead-of-substrings-it-barely-helps\">Upgrade 1 — a judge instead of substrings. It barely helps.<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#upgrade-1--a-judge-instead-of-substrings-it-barely-helps\" class=\"hash-link\" aria-label=\"Direct link to Upgrade 1 — a judge instead of substrings. It barely helps.\" title=\"Direct link to Upgrade 1 — a judge instead of substrings. It barely helps.\" translate=\"no\">​</a></h3>\n<p>Substring matching (<code>icontains</code>) was perfect for part 2's single JSON field and is <strong>too brittle\nfor long-form RAG answers</strong>. The obvious upgrade is part 4's skill: an <strong>LLM judge</strong> grading\nsemantic consistency (<code>llm-rubric:</code> assertions + a judge model in <code>defaultTest</code>). We did exactly\nthat — same 37 labels, judge instead of substrings — and got:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (judge, uncurated labels)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Results:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ✓ 17 passed (44.74%)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ✗ 21 failed (55.26%)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Duration: 8m 21s (concurrency: 4)</span><br></div></code></pre></div></div>\n<p><strong>39.5% → 44.7%.</strong> Barely moved — because <strong>the judge faithfully enforces bad labels.</strong> Rows with\ngarbage <code>key_fact</code>s still fail: the reference is wrong, not the answer. A better grader cannot\nrescue a bad dataset. The bottleneck was never the assertion type.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"upgrade-2--curate-the-labels-with-a-gate\">Upgrade 2 — curate the labels with a gate<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#upgrade-2--curate-the-labels-with-a-gate\" class=\"hash-link\" aria-label=\"Direct link to Upgrade 2 — curate the labels with a gate\" title=\"Direct link to Upgrade 2 — curate the labels with a gate\" translate=\"no\">​</a></h3>\n<p>So fix the data (part 3's discipline, now enforced by code). Generator v2 demands the label be\n<em>the answer</em> — not a menu path, not a section name — and adds a <strong>curation gate</strong>: the <code>key_fact</code>\nmust exist <strong>verbatim</strong> in the source text, or the row is rejected on the spot:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> app/gen_qa.py </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">\"\"\"Generate a QA eval set from the indexed docs — v2, curated labels.\"\"\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">import os, json, chromadb</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">from openai import OpenAI</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">wec = OpenAI(base_url=\"https://inference.wiline.com/v1\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">             api_key=os.environ[\"WEC_API_KEY\"], timeout=60)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">db = chromadb.PersistentClient(path=\"/data\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">col = db.get_or_create_collection(\"wec-docs\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">data = col.get(include=[\"documents\", \"metadatas\"])</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">pages = {}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">for doc, meta in zip(data[\"documents\"], data[\"metadatas\"]):</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    pages.setdefault(meta[\"url\"], []).append(doc)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">kept, skipped = 0, 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">with open(\"/srv/app/qa.jsonl\", \"w\") as f:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    for url, chunks in sorted(pages.items()):</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        ctx = \"\\n\".join(chunks[:2])[:2000]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        resp = wec.chat.completions.create(</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            model=\"Qwen2.5-3B-Instruct\", temperature=0.3,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            messages=[{\"role\": \"user\", \"content\":</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">                \"From this documentation excerpt, write ONE question a customer would ask, \"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">                \"and the fact that ANSWERS it. Return ONLY JSON with keys:\\n\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">                \"question (string), key_fact (string).\\n\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">                \"Rules for key_fact: it must be THE ANSWER to the question (not a menu path, \"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">                \"not a section name), a short phrase copied VERBATIM from the excerpt, \"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">                \"max 8 words.\\n\\n\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">                f\"Excerpt:\\n{ctx}\"}],</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        )</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        raw = resp.choices[0].message.content</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        try:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            start, end = raw.index(\"{\"), raw.rindex(\"}\") + 1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            row = json.loads(raw[start:end])</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            # curation gate: the label must literally exist in the source text</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            if row[\"key_fact\"].lower().strip() not in ctx.lower():</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">                print(f\"SKIP {url.split('/docs/')[1]}: key_fact not verbatim in source\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">                skipped += 1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">                continue</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            row[\"url\"] = url</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            f.write(json.dumps(row) + \"\\n\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            kept += 1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            print(f\"ok   {url.split('/docs/')[1]}: {row['question'][:50]} -&gt; {row['key_fact'][:40]}\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        except Exception as e:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            print(f\"SKIP {url}: {e}\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">            skipped += 1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">print(f\"done -&gt; {kept} kept, {skipped} rejected by curation gate\")</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> rag-api python </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> app.gen_qa</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (abridged)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ok   cloud_portal/management/billing/how_billing_works/: What is the starting point ... -&gt; How Billing Works</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">SKIP cloud_portal/management/billing/overview/: key_fact not verbatim in source</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ok   cloud_portal/platform/inference/api/wiline-edge-cloud-inference-api/: What is the base URL ... -&gt; https://inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">SKIP cloud_portal/platform/inference/api_keys/: key_fact not verbatim in source</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">done -&gt; 15 kept, 22 rejected by curation gate</span><br></div></code></pre></div></div>\n<p><strong>15 kept, 22 rejected.</strong> That rejection rate is the point: more than half of what the generator\nproduced would have graded the RAG against wrong references. A smaller, trustworthy dataset beats\na bigger, poisoned one.</p>\n<p>The generator samples at temperature 0.3, so the exact tally shifts slightly between runs — here's\na later regeneration that kept 16 of 37; the rejection rate stays brutal either way:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The curation gate — verbatim-verified pairs kept, the rest rejected\" src=\"https://development-wec.wiline.com/docs/assets/images/rag-qa-gen-8dfb631c47f8fc4cb9c506fef0f651d2.png\" width=\"946\" height=\"852\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 7.</strong> The gate at work: 16 kept, 21 rejected on this run. Every surviving label is provably\nin the docs — real answer-facts like <code>audio/mpeg</code> and the API base URL, not menu paths. (The\ndocumented eval runs below use our first curated set: 15 pairs + the regression = 16 tests.)</p>\n<p>Rebuild <code>tests.csv</code> with a rubric that gives the judge the question <strong>and</strong> the verified\nreference, plus explicit PASS/FAIL rules (different wording is fine; contradiction fails):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'question,__expected'</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'[.question, (\"llm-rubric:Question asked: \\\"\" + .question + \"\\\". Reference fact from the official docs: \\\"\" + .key_fact + \"\\\". PASS if the answer correctly addresses the question and does not contradict the reference fact — different wording is fine. FAIL only if the answer is wrong, contradicts the reference, or fails to answer the question.\")] | @csv'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">..</span><span class=\"token plain\">/app/qa.jsonl</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> tests.csv</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (curated labels + judge)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Results:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ✓ 12 passed (75.00%)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ✗ 4 failed (25.00%)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Duration: 2m 47s (concurrency: 4)</span><br></div></code></pre></div></div>\n<p><strong>75%.</strong> Better — but before celebrating, read the four failures. And here the series' habit pays\noff one more time, because <strong>three of them weren't RAG failures either.</strong></p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"upgrade-3--the-judge-itself-was-breaking\">Upgrade 3 — the judge itself was breaking<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#upgrade-3--the-judge-itself-was-breaking\" class=\"hash-link\" aria-label=\"Direct link to Upgrade 3 — the judge itself was breaking\" title=\"Direct link to Upgrade 3 — the judge itself was breaking\" translate=\"no\">​</a></h3>\n<p>The three failures whose grader reason was just <em>\"No output\"</em> all shared two tells:\n<code>graderError: true</code>, and a completion of <strong>exactly 1024 tokens</strong> — Promptfoo's default\n<code>max_tokens</code>. Our judge is a thinking model: it spent the whole budget reasoning and got <strong>cut off\nbefore emitting the verdict JSON</strong>, which Promptfoo counts as FAIL. The fourth \"failure\" got a\nverdict — but the small judge had literally echoed the schema placeholder (<code>reason: \"string\"</code>)\ninstead of grading. Both answers were actually correct.</p>\n<p>Two config lines fix it — a bigger judge, and room to think:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">eval/promptfooconfig.yaml (judge block, final)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">defaultTest</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">options</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">provider</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">id</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> openai</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">chat</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">Qwen3.5</span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">122B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">config</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">apiBaseUrl</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> https</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">//inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">apiKeyEnvar</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> WEC_API_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">temperature</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># thinking models spend tokens on reasoning before the verdict JSON;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># the 1024 default silently truncated the judge and failed 3 tests</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        </span><span class=\"token key atrule\" style=\"color:#00a4db\">max_tokens</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">4096</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (final run)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Results:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ✓ 14 passed (87.50%)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ✗ 2 failed (12.50%)</span><br></div></code></pre></div></div>\n<p>And the row that matters most:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">│ What is the pricing model for GPU hourly usage in Edge Cloud?  │ [PASS] ... starting at $0.66/GPU-hour. │</span><br></div></code></pre></div></div>\n<p><strong>The regression is green</strong> — the Step-4 conflation is pinned and cannot quietly return. (One\ncosmetic note: the 122B judge sometimes keeps its reason text to a terse <code>...</code> — the verdicts\nthemselves are correct, which is what the gate reads.)</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can gate a live RAG service in CI: generated + gate-curated QA, a judge for semantics,\ndeterministic regressions for known bugs — and you know how to debug the judge itself.</p></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"upgrade-4--the-875-didnt-survive-a-re-run\">Upgrade 4 — the 87.5% didn't survive a re-run<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#upgrade-4--the-875-didnt-survive-a-re-run\" class=\"hash-link\" aria-label=\"Direct link to Upgrade 4 — the 87.5% didn't survive a re-run\" title=\"Direct link to Upgrade 4 — the 87.5% didn't survive a re-run\" translate=\"no\">​</a></h3>\n<p>One habit this series drills: <strong>re-run before you believe.</strong> We did — and got <strong>75%</strong>, with\n<em>different</em> rows failing than before. Billing, which had just passed, now failed with a circular\necho; the TTS answer said \"mp3\" one run and \"unspecified\" the next. The score was a lottery\nbecause <strong>the service itself was non-deterministic</strong>: <code>generate()</code> passed no <code>temperature</code>, so\nevery eval graded a different roll of the dice at the default 1.0.</p>\n<p>One line ends the lottery — add to the <code>create()</code> call in <code>generate()</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        temperature</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\">               </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># determinism: same question -&gt; same answer</span><br></div></code></pre></div></div>\n<p>Re-run twice. <strong>13/16 — 81.25% — both times, with the same three failures.</strong> The number went\n<em>down</em> and that's the win: 87.5% was partly luck (the flaky rows happened to land right that run);\n81.25% is reproducible. A reader who re-runs your eval should get your number.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-three-real-failures--finally-actual-rag-bugs\">The three real failures — finally, actual RAG bugs<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#the-three-real-failures--finally-actual-rag-bugs\" class=\"hash-link\" aria-label=\"Direct link to The three real failures — finally, actual RAG bugs\" title=\"Direct link to The three real failures — finally, actual RAG bugs\" translate=\"no\">​</a></h3>\n<p>After four rounds of fixing the <em>measurement</em>, what's left is signal — stable across runs, and\neach one is a different <strong>classic RAG failure type</strong>:</p>\n<ol>\n<li class=\"\"><strong>Subnets (\"Total Networks\") — retrieval miss, chunk boundary.</strong> The right page ranks, but dump\nthe top-4 retrieved chunks and none contains the stats block with the answer; the top chunk\nstarts <strong>mid-word</strong> (<code>\"iated with subnets\\nData Transfer…\"</code>). Fixed-size character chunking\nsplit the fact across a boundary, and the half with the answer doesn't rank. Fix class: larger\noverlap, bigger <code>k</code>, or structure-aware chunking.</li>\n<li class=\"\"><strong>TTS default format — retrieval miss, fact not in top-k.</strong> The chunk stating the default never\nreaches the context. Revealing detail: at temperature 1.0 this row <em>sometimes passed</em> — the\nmodel guessed \"mp3\" without evidence. Determinism converted a lucky guess into an honest,\nconsistent failure.</li>\n<li class=\"\"><strong>Billing (\"How Billing Works\") — generation quality.</strong> At temperature 0 the small model\ndeterministically echoes the source sentence — <em>\"This is the starting point for billing…\"</em> —\nwithout naming the page. Correct refusal-free retrieval, useless circular answer; the judge\nrightly fails it. Fix class: prompt work (\"name the page/section you're citing\") or a stronger\ngeneration model.</li>\n</ol>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"spend-the-signal--one-parameter-chosen-by-the-eval\">Spend the signal — one parameter, chosen by the eval<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#spend-the-signal--one-parameter-chosen-by-the-eval\" class=\"hash-link\" aria-label=\"Direct link to Spend the signal — one parameter, chosen by the eval\" title=\"Direct link to Spend the signal — one parameter, chosen by the eval\" translate=\"no\">​</a></h3>\n<p>A trustworthy eval isn't the goal; it's the <em>map</em>. It just told us two of three failures are the\nsame class — facts that don't reach the top-4 retrieved chunks. The cheapest fix in that class is\none character, in <code>retrieve()</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">retrieve</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">question</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> k</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">int</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">6</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\">     </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># was 4 — two misses said \"not enough context\"</span><br></div></code></pre></div></div>\n<p>Re-run. Then re-run again, because that's the habit now:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (both runs)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Results:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ✓ 14 passed (87.50%)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ✗ 2 failed (12.50%)</span><br></div></code></pre></div></div>\n<p><strong>14/16 — 87.5% — twice, same two failures.</strong> Read what happened with each:</p>\n<ul>\n<li class=\"\"><strong>TTS default — fixed.</strong> With six chunks the page stating the default reaches the context, and\nthe service now answers \"mp3\" <em>with evidence</em>, every run — the lucky guess became a grounded\nanswer.</li>\n<li class=\"\"><strong>Subnets — survived.</strong> Even at <code>k=6</code> the chunk with the stats block doesn't rank: this miss is\n<em>semantic</em>, not budgetary — the boundary-mangled text (<code>\"iated with subnets…\"</code>) embeds poorly.\nMore <code>k</code> can't fix what chunking broke; this one genuinely needs structure-aware chunking.</li>\n<li class=\"\"><strong>Billing — survived, as predicted.</strong> It's a generation-quality failure; no retrieval parameter\nwas ever going to touch it.</li>\n</ul>\n<p>The number is back to 87.5% — the same as the lucky run — <strong>but this time it reproduces</strong>, and\nevery point of it has an explanation.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Final eval — deterministic service, k=6, judge-graded, regression passing\" src=\"https://development-wec.wiline.com/docs/assets/images/rag-eval-final-eb3776d3a5abed5af34d6896bbee06d0.png\" width=\"965\" height=\"976\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 8.</strong> The final run: 14/16, the same two failures every time, regression PASS.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"read-the-final-number-honestly\">Read the final number honestly<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#read-the-final-number-honestly\" class=\"hash-link\" aria-label=\"Direct link to Read the final number honestly\" title=\"Direct link to Read the final number honestly\" translate=\"no\">​</a></h3>\n<p>39.5% → 44.7% → 75% → 87.5% <em>(lucky)</em> → 81.25% <em>(honest)</em> → <strong>87.5% (earned)</strong>. Until the very\nlast step, the RAG barely changed — what changed was the <strong>measurement</strong>: assertions (substrings →\njudge), labels (curation gate), the judge's own config (truncation), determinism (temperature).\nOnly then did one retrieval parameter, chosen <em>by</em> the eval, actually move quality. That's the\nlast lesson of the series:</p>\n<ul>\n<li class=\"\"><strong>Most \"failures\" were our eval lying to us</strong> — bad labels, brittle asserts, a silently\ntruncated judge, a dice-rolling service. Read the rows before you touch the RAG.</li>\n<li class=\"\"><strong>The honest number was lower than the lucky one — and worth more</strong>, because it reproduces. If\nyour score changes when you re-run, you don't have a score yet.</li>\n<li class=\"\"><strong>Fix the measurement first, then spend its signal.</strong> The same 87.5% appears twice in this arc;\nonly the second one means something — it survives re-runs and every point has an explanation.</li>\n<li class=\"\"><strong>The score is a trend instrument; the rows are the truth.</strong> 87.5% still isn't \"the RAG's\nquality\" — it's this dataset, this judge, this corpus. Pin what must never regress with\ndeterministic asserts; let the judge track the rest.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-you-built\">What you built<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#what-you-built\" class=\"hash-link\" aria-label=\"Direct link to What you built\" title=\"Direct link to What you built\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">A <strong>Docker-first RAG service</strong>: scrape (sitemap → article extraction) → chunk → <strong>local\nembeddings</strong> (fastembed/ONNX) → Chroma → <strong>WEC generation</strong>, behind <code>POST /ask</code> with\ndeterministic citations.</li>\n<li class=\"\">A <strong>real bug story</strong>: probed the negative case, caught a garbled GPU price (a storage rate welded\nonto the GPU prepay option), root-caused it to the scraper's flattened structure, fixed ingestion<!-- -->\n<ul>\n<li class=\"\">hardened the prompt, and <strong>pinned it with a regression test that now passes</strong>.</li>\n</ul>\n</li>\n<li class=\"\">A <strong>CI-shaped eval</strong> of the live endpoint: gate-curated QA (15 verbatim-verified pairs out of 37\ngenerated) + a judge you debugged like any other component — 39.5% → <strong>87.5% reproducible</strong>, by\nfixing the <em>measurement</em> first (assertions, labels, judge config, determinism) and then spending\nits signal on one eval-chosen retrieval parameter. Two real, nameable failures remain — on\npurpose, with fix classes named.</li>\n<li class=\"\"><strong>Full observability</strong>: every request a span tree in Langfuse — retrieval and generation\nseparately debuggable.</li>\n</ul>\n<p>Parts 1–4 were the toolkit. This is what the toolkit is <em>for</em>.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>RAG, end to end</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting\">Troubleshooting<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#troubleshooting\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting\" title=\"Direct link to Troubleshooting\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"ask-returns-nothing-after-re-indexing\"><code>/ask</code> returns nothing after re-indexing<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#ask-returns-nothing-after-re-indexing\" class=\"hash-link\" aria-label=\"Direct link to ask-returns-nothing-after-re-indexing\" title=\"Direct link to ask-returns-nothing-after-re-indexing\" translate=\"no\">​</a></h3>\n<p>If you drop and recreate the Chroma collection while the API is running, the service holds a\n<strong>stale collection handle</strong> and every query throws:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">chromadb.errors.InvalidCollectionException: Collection 9b486740-… does not exist.</span><br></div></code></pre></div></div>\n<p>The running process cached the old collection's UUID. Restart the service\n(<code>docker compose restart rag-api</code>) — or resolve the collection per-request instead of at startup.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The stale-collection error after re-indexing\" src=\"https://development-wec.wiline.com/docs/assets/images/rag-stale-handle-1639bfb9ef1938577ff551daa191a889.png\" width=\"942\" height=\"529\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 9.</strong> Re-indexing invalidates open handles — a classic \"works until you rebuild the index\"\nproduction trap.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"failed-to-send-telemetry-event--capture-takes-1-positional-argument\"><code>Failed to send telemetry event … capture() takes 1 positional argument</code><a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#failed-to-send-telemetry-event--capture-takes-1-positional-argument\" class=\"hash-link\" aria-label=\"Direct link to failed-to-send-telemetry-event--capture-takes-1-positional-argument\" title=\"Direct link to failed-to-send-telemetry-event--capture-takes-1-positional-argument\" translate=\"no\">​</a></h3>\n<p>Harmless chromadb/posthog version clash. Set <code>ANONYMIZED_TELEMETRY: \"False\"</code> in the environment\n(already in our compose).</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"judge-failures-with-reason-no-output-and-gradererror-true\">Judge failures with reason \"No output\" (and <code>graderError: true</code>)<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#judge-failures-with-reason-no-output-and-gradererror-true\" class=\"hash-link\" aria-label=\"Direct link to judge-failures-with-reason-no-output-and-gradererror-true\" title=\"Direct link to judge-failures-with-reason-no-output-and-gradererror-true\" translate=\"no\">​</a></h3>\n<p>Check the grading record's completion tokens: if it's <strong>exactly 1024</strong> — Promptfoo's default\n<code>max_tokens</code> — your thinking-model judge spent the whole budget reasoning and was cut off before\nthe verdict JSON. Set <code>max_tokens: 4096</code> on the judge provider. Related tell: a small judge\nreturning <code>reason: \"string\"</code> is echoing the schema placeholder instead of grading — use a bigger\njudge.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-judge-eval-is-slow-or-verdicts-look-random\">The judge eval is slow or verdicts look random<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#the-judge-eval-is-slow-or-verdicts-look-random\" class=\"hash-link\" aria-label=\"Direct link to The judge eval is slow or verdicts look random\" title=\"Direct link to The judge eval is slow or verdicts look random\" translate=\"no\">​</a></h3>\n<p>Reasoning judges are slow (~minutes for a dozen rows) and imperfect — expect occasional verdict\nflips between runs. Keep <strong>deterministic assertions for known bugs</strong> (they're free and never flip)\nand treat the judge score as a trend, not a truth. If a judge row hangs entirely, it's usually a\ntransient network hiccup, not your config — retry before debugging.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"docker-build-fails-with-no-space-left-on-device\">Docker build fails with \"no space left on device\"<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#docker-build-fails-with-no-space-left-on-device\" class=\"hash-link\" aria-label=\"Direct link to Docker build fails with &quot;no space left on device&quot;\" title=\"Direct link to Docker build fails with &quot;no space left on device&quot;\" translate=\"no\">​</a></h3>\n<p>The image needs ~1 GB (chromadb + onnxruntime are the heavy layers). Check <code>df -h /</code> first and\n<code>docker system df</code> for reclaimable space — but <strong>never prune blindly on a shared box</strong>: \"reclaimable\"\nlies when other projects' containers are running.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>The honest limitations are the roadmap, and each one traces to a failure you watched happen:\n<strong>structure-aware chunking</strong> (the subnets miss survived <code>k=6</code> — the boundary-mangled chunk embeds\npoorly, so no retrieval budget rescues it), <strong>prompt or model work on generation</strong> (the billing\necho: force the model to name the page it's citing, or use a stronger generator), <strong>table-aware\nextraction</strong> (the pricing strip is still fragment soup — the regression passes, but \"$1.44/mo\"\nstill bleeds near the GPU line; add a <em>positive</em> assert for the correct figure, not just\n<code>not-icontains</code> for the wrong ones), and <strong>judge calibration</strong> (grade the judge against a handful\nof human verdicts). Each one moves the pass rate for a reason you can name — which, after five\nparts, is the whole point: <em>you don't guess whether your AI works. You measure it, watch it, and\nmake it prove itself.</em></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://docs.trychroma.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Chroma docs — persistent client &amp; collections</a></li>\n<li class=\"\"><a href=\"https://github.com/qdrant/fastembed\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">fastembed — ONNX embeddings without PyTorch</a></li>\n<li class=\"\"><a href=\"https://www.promptfoo.dev/docs/providers/http/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Promptfoo — HTTP provider</a></li>\n<li class=\"\"><a href=\"https://langfuse.com/docs/sdk/python/decorators\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Langfuse — Python decorators</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/",
            "title": "The capstone: build, evaluate, and observe a RAG docs assistant on the WEC API",
            "summary": "Build a production-shaped RAG service in Docker: scrape a real docs site, embed locally, generate on the WEC Inference API — then catch a real hallucination, root-cause it to your own scraper, fix it, and pin it with a regression test. Every command, number, and error is real.",
            "date_modified": "2026-07-13T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "rag",
                "embeddings",
                "chromadb",
                "docker",
                "evals",
                "promptfoo",
                "langfuse",
                "inference"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/",
            "content_html": "<div class=\"tutorialHero\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"2255\" height=\"527\" fill=\"none\" viewBox=\"0 0 2255 527\" class=\"tutorialHero__langfuse\"><path fill=\"#1B1917\" d=\"M652.669 116.433c0-10.261-7.683-17.956-17.926-17.956H607V60h87.923v316.366h-42.254zM804.056 379.786c-39.693 0-72.131-27.362-72.131-68.831 0-41.042 28.596-69.686 84.935-69.686h39.693c7.256 0 12.805-5.558 12.805-12.826v-8.55c0-25.651-20.487-38.905-40.547-38.905-18.353 0-33.291 8.123-41.828 26.934h-45.668c11.95-44.462 46.095-65.41 88.349-65.41 40.547 0 82.374 22.231 82.374 77.808v156.046h-42.68v-14.109c0-5.13-5.549-7.695-9.817-4.702-16.646 11.97-32.438 22.231-55.485 22.231m5.975-38.477c18.78 0 34.572-9.406 52.498-27.362 4.695-4.702 6.829-10.688 6.829-17.1v-6.413c0-7.268-5.549-12.826-12.805-12.826H815.58c-27.743 0-40.974 12.398-40.974 31.637 0 17.956 12.377 32.064 35.425 32.064M961.855 376.366V147.642h42.255v19.666c0 5.13 6.4 6.84 10.24 2.992 13.23-12.825 31.59-27.788 60.61-27.788 36.71 0 70.85 23.086 70.85 75.671v158.183h-42.25V227.161c0-29.499-17.93-45.745-39.7-45.745-20.91 0-35 11.971-49.93 29.499-7.26 8.978-9.82 19.238-9.82 30.354v135.097zM1287.48 467c-52.5 0-85.79-24.796-95.61-63.273h46.1c7.25 15.818 20.06 25.651 45.67 25.651 34.14 0 55.48-20.093 55.48-65.838v-7.268c0-5.13-4.27-7.695-9.39-3.42-14.08 12.398-32.01 20.093-49.08 20.093-58.05 0-96.46-44.889-96.46-115.003 0-70.113 44.39-115.43 98.17-115.43 15.79 0 30.3 4.275 44.38 14.963 5.98 4.275 12.38.855 12.38-5.986v-3.847h42.26V363.54c0 72.678-43.97 103.46-93.9 103.46m-2.56-132.959c19.2 0 33.29-8.55 44.81-20.949 7.26-8.122 9.39-13.68 9.39-26.506v-61.563c0-12.826-2.13-20.948-10.67-29.071-9.39-8.978-22.62-14.964-39.69-14.964-35 0-61.89 29.072-61.89 76.954 0 47.883 24.76 76.099 58.05 76.099M1455.92 199.372c0-7.268-5.97-13.253-13.23-13.253h-32.44v-38.477h32.44c7.26 0 13.23-5.985 13.23-13.253v-5.986c0-45.744 23.48-68.403 69.15-68.403h29.02v38.477h-29.45c-17.5 0-26.46 9.833-26.46 29.926v5.986c0 7.268 5.97 13.253 13.23 13.253h42.68v38.477h-42.68c-7.26 0-13.23 5.985-13.23 13.253v176.994h-42.26zM1652.02 381.496c-35.85 0-69.14-23.086-69.14-75.671V147.642h42.25v150.06c0 29.499 17.07 44.889 37.13 44.889 21.77 0 35.85-11.97 50.79-29.499 7.26-8.977 9.82-19.238 9.82-30.354V147.642h42.25v228.724h-42.25V356.7c0-5.131-6.4-6.841-10.24-2.993-13.24 12.826-31.59 27.789-60.61 27.789M1893.57 381.496c-38.84 0-79.39-19.239-90.06-65.838h43.54c6.4 17.528 23.9 29.498 44.81 29.498 23.05 0 37.13-13.68 37.13-30.353 0-16.246-11.09-25.224-28.59-30.354l-36.28-10.261c-31.58-8.978-55.06-29.499-55.06-64.556 0-38.049 35.43-67.12 75.55-67.12 32.01 0 70.85 14.535 81.09 65.41h-40.55c-5.55-17.528-20.06-29.071-40.54-29.071-20.06 0-34.58 12.398-34.58 28.216 0 13.253 8.11 23.086 27.32 28.644l34.15 9.833c32.43 9.406 58.47 29.072 58.47 66.693 0 39.332-34.15 69.259-76.4 69.259M2098.54 381.496c-61.46 0-102.01-51.73-102.01-119.706s43.11-119.278 101.58-119.278c63.6 0 96.89 51.302 96.89 109.872v23.087h-144.26c-5.98 0-8.54 3.847-7.26 13.68 4.7 32.064 30.31 54.295 55.49 54.295 18.78 0 35-9.405 45.67-27.788h44.81c-16.22 40.187-49.94 65.838-90.91 65.838m43.11-141.51c6.83 0 9.39-3.42 7.68-14.108-4.69-26.506-24.33-45.317-51.22-45.317-25.6 0-47.37 18.811-54.2 45.745-2.56 9.833.85 13.68 6.83 13.68z\"></path><path fill=\"#FF5D5F\" d=\"m286.292 286.105 34.597 27.791s26.473-19.661 45.941-22.545c20.418-3.025 42.202 8.359 62.388 21.93 30.489 20.498 56.149 46.508 56.149 46.508l30.06-29.493s-82.879-89.795-148.597-81.672c-43.105 5.328-80.538 37.481-80.538 37.481\"></path><path fill=\"#4E9CFF\" d=\"M88.358 114.862 60 146.056s79.009 73.732 141.224 73.732c28.358 0 67.684-22.216 101.523-51.079 19.283-16.448 40.835-35.13 62.388-35.13 14.487 0 33.594 7.673 51.612 27.824 0 0 11.63-6.974 18.716-11.985 6.228-4.404 15.479-11.91 15.479-11.91-25.918-27.663-63.407-47.883-85.807-45.9-36.299.005-62.388 22.601-94.717 48.735s-45.94 36.907-69.194 36.907c-39.134 0-112.866-62.388-112.866-62.388M88.358 352.463 60 321.269s79.009-73.732 141.224-73.732c28.358 0 67.684 22.216 101.523 51.079 19.283 16.448 40.835 35.13 62.388 35.13 14.556 0 33.518-7.989 51.612-28.358 0 0 10.877 6.705 17.582 11.344 6.894 4.769 17.015 12.655 17.015 12.655-25.931 27.883-63.693 48.323-86.209 46.33-36.299-.005-57.851-19.24-90.179-45.374-32.329-26.133-50.478-40.268-73.732-40.268-39.134 0-112.866 62.388-112.866 62.388M458.142 185.149c-7.378 5.1-19.283 12.478-19.283 12.478s6.806 14.746 6.806 34.597-6.239 36.866-6.239 36.866 10.688 6.675 17.582 11.343c7.162 4.849 18.149 13.045 18.149 13.045s13.045-27.224 13.045-61.254-13.045-59.552-13.045-59.552-10.236 7.792-17.015 12.477\"></path><path fill=\"#FF5D5F\" d=\"m287.995 180.612 32.895-27.224s26.473 19.046 45.941 21.93c20.417 3.026 42.202-8.359 62.388-21.93 30.489-20.498 56.149-46.507 56.149-46.507l30.06 29.492s-82.879 89.795-148.597 81.672c-43.105-5.328-78.836-37.433-78.836-37.433M208.601 91c42.538 0 78.264 36.299 78.264 36.299s-9.941 7.832-16.448 13.045c-6.777 5.429-17.582 14.179-17.582 14.179s-18.711-19.851-44.234-19.851c-10.465 0-24.066 6.286-38.567 18.716-11.188 9.591-22.829 21.514-30.627 36.299-6.743 12.784-10.42 27.85-10.776 43.672-.446 19.873 6.597 40.704 18.149 57.283 7.743 11.112 16.983 19.474 26.657 26.657 12.555 9.322 25.648 15.881 35.164 15.881 10.166 0 19.306-3.533 26.09-6.806 10.776-6.239 19.278-13.612 19.278-13.612l33.463 27.791s-13.612 13.612-32.323 23.821c-12.091 5.963-27.632 11.91-46.508 11.91-18.862 0-40.767-10.022-61.254-25.522-13.244-10.021-26.225-21.895-36.298-36.299-16.51-23.607-25.017-52.328-24.96-81.104.057-29.136 9.451-57.993 26.094-81.672C138.273 117.657 176.86 91 208.601 91\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 8 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->8</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->8<!-- --> earned</span></div><div class=\"skillTracker__series\">AI evals &amp; observability</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove a model works</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trustworthy JSON</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real test data at scale</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">4</span><span class=\"skillTracker__skill\" data-state=\"current\">Observe &amp; score production</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">RAG, end to end</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">Catch regressions in CI</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trace &amp; debug agent tool calls</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/web-search-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Add live web search</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>A customer says your support bot promised them a refund policy that doesn't exist. Your feature\nmade <strong>two</strong> LLM calls — classify, then reply. Which one invented it? If you can't answer that,\nyour app is a black box — and this guide fixes exactly that.</p>\n<p>You can now prove a model works (<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\">part 1</a>),\nmake its output machine-reliable (<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/\">part 2</a>),\nand generate a real test set to check it against\n(<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/\">part 3</a>). But all of that runs <strong>offline,\nin CI, on inputs you chose.</strong> Production doesn't play along.</p>\n<p>This guide closes the gap. We'll self-host <strong>Langfuse</strong> — the open-source, self-hostable\nalternative to LangSmith — trace every real call, <strong>auto-score live traffic with an LLM judge</strong>,\ndrill into the exact step that breaks, and <strong>feed failures back</strong> so your part-3 dataset gets\nstronger. Offline eval tells you it worked on your test set; this tells you it works in the wild.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-skill-this-adds\">The skill this adds<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#the-skill-this-adds\" class=\"hash-link\" aria-label=\"Direct link to The skill this adds\" title=\"Direct link to The skill this adds\" translate=\"no\">​</a></h2>\n<p>By the end you can take any WEC feature and: <strong>see inside every call, score quality on real\ntraffic automatically, find the exact call that broke and why, and turn production failures into\nnew test cases.</strong> That's the difference between \"it passed CI\" and \"it's holding up in production.\"</p>\n<p>We'll keep the running example from part 3 — a <strong>support-ticket classifier</strong> on the WEC Inference\nAPI — and put it under observation.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-were-building\">What we're building<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#what-were-building\" class=\"hash-link\" aria-label=\"Direct link to What we're building\" title=\"Direct link to What we're building\" translate=\"no\">​</a></h2>\n<p>Your app calls the WEC API as usual; Langfuse (self-hosted) quietly captures <strong>every call as a\ntrace</strong>, an LLM judge <strong>scores</strong> each one, and the failures flow <strong>back into your part-3\ndataset</strong> — so production keeps making your evals stronger:</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"why-langfuse-and-when-to-pick-something-lighter\">Why Langfuse (and when to pick something lighter)<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#why-langfuse-and-when-to-pick-something-lighter\" class=\"hash-link\" aria-label=\"Direct link to Why Langfuse (and when to pick something lighter)\" title=\"Direct link to Why Langfuse (and when to pick something lighter)\" translate=\"no\">​</a></h2>\n<p>We use Langfuse because it's the open-source leader for LLM observability (MIT-licensed,\nself-hostable), its SDK is <strong>OpenAI-compatible</strong> so it points at the WEC API unchanged, and — the\nreason it fits <em>this</em> series — it has <strong>datasets, scoring, and LLM-as-judge evaluators built in.</strong>\nThat's what lets your part-3 dataset and the \"score production, feed failures back\" loop be\nfirst-class instead of bolted on.</p>\n<p>The trade-off: the production stack is heavy (Postgres + ClickHouse). If you don't need the eval\nfeatures and want lighter, reasonable alternatives are <strong>Arize Phoenix</strong> (OpenTelemetry-native,\nstrong for offline eval), <strong>Helicone</strong> (a one-line proxy — but no evals), or <strong>OpenLIT</strong>\n(single-binary, OTEL-native). For an eval-driven workflow like this one, the extra containers earn\ntheir keep; for plain tracing, one of those may suit you better.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--self-host-langfuse-on-a-wec-instance\">Step 1 — Self-host Langfuse on a WEC Instance<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#step-1--self-host-langfuse-on-a-wec-instance\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Self-host Langfuse on a WEC Instance\" title=\"Direct link to Step 1 — Self-host Langfuse on a WEC Instance\" translate=\"no\">​</a></h2>\n<p>Langfuse's self-host stack is six services (web + worker, ClickHouse, Postgres, Redis, object\nstore), so give it a WEC Instance with <strong>~15 GB disk and 4 GB+ RAM</strong> — don't cram it onto a box\nthat's already busy.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">git</span><span class=\"token plain\"> clone https://github.com/langfuse/langfuse.git </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> langfuse</span><br></div></code></pre></div></div>\n<p>Generate three secrets and drop them in a <code>.env</code> (the <code>ENCRYPTION_KEY</code> <strong>must</strong> be hex, or the\nweb container won't boot):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> .env </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">NEXTAUTH_SECRET=</span><span class=\"token string variable\" style=\"color:#36acaa\">$(</span><span class=\"token string variable\" style=\"color:#36acaa\">openssl rand </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-base64</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable number\" style=\"color:#36acaa\">32</span><span class=\"token string variable\" style=\"color:#36acaa\">)</span><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">SALT=</span><span class=\"token string variable\" style=\"color:#36acaa\">$(</span><span class=\"token string variable\" style=\"color:#36acaa\">openssl rand </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-base64</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable number\" style=\"color:#36acaa\">32</span><span class=\"token string variable\" style=\"color:#36acaa\">)</span><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">ENCRYPTION_KEY=</span><span class=\"token string variable\" style=\"color:#36acaa\">$(</span><span class=\"token string variable\" style=\"color:#36acaa\">openssl rand </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-hex</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable number\" style=\"color:#36acaa\">32</span><span class=\"token string variable\" style=\"color:#36acaa\">)</span><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">NEXTAUTH_URL=http://&lt;your-instance-ip&gt;:3000</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Langfuse stack up — six services healthy\" src=\"https://development-wec.wiline.com/docs/assets/images/langfuse-deploy-ps-597bc541cd07b60e848e6c0dfe6246fc.png\" width=\"2524\" height=\"422\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> <code>docker compose ps</code> — all six services (web, worker, ClickHouse, Postgres, Redis, MinIO) up and healthy.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Deploying next to other services</div><div class=\"admonitionContent_BuS1\"><p>If the box already runs something on port 3000 or 5432, Langfuse won't start (\"address already\nin use\"). The backing databases don't need host ports at all — edit <code>docker-compose.yml</code> to move\n<code>langfuse-web</code> to a free host port (e.g. <code>3001:3000</code>) and drop the Postgres host mapping. Keeping\nyour databases off the host network is good practice anyway. See <a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#ts-ports\" class=\"\">Troubleshooting</a>.</p></div></div>\n<p>Open <code>http://&lt;instance-ip&gt;:3000</code>, create an account (first user is the owner), then an\n<strong>Organization → Project</strong>, and under <strong>Settings → API Keys</strong> create a key pair\n(<code>pk-lf-…</code> / <code>sk-lf-…</code>).</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--instrument-the-classifier-so-every-call-is-traced\">Step 2 — Instrument the classifier so every call is traced<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#step-2--instrument-the-classifier-so-every-call-is-traced\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Instrument the classifier so every call is traced\" title=\"Direct link to Step 2 — Instrument the classifier so every call is traced\" translate=\"no\">​</a></h2>\n<p>Point the Langfuse SDK at your instance and use its <strong>OpenAI drop-in</strong> — it auto-traces any\nOpenAI-compatible call, including the WEC Inference API, with zero extra code around each request:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">python3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> venv ~/lf-env </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> ~/lf-env/bin/activate</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">pip </span><span class=\"token function\" style=\"color:#d73a49\">install</span><span class=\"token plain\"> langfuse openai</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">LANGFUSE_PUBLIC_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"pk-lf-…\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">LANGFUSE_SECRET_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"sk-lf-…\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">LANGFUSE_HOST</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"http://&lt;instance-ip&gt;:3000\"</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># use the port you exposed — 3001 if you remapped in Step 1</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">WEC_API_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"sk-your-wec-key\"</span><br></div></code></pre></div></div>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">app.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langfuse</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">openai </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> openai          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># drop-in: auto-traces every call</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langfuse </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> get_client</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">client </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> openai</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">OpenAI</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    base_url</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"https://inference.wiline.com/v1\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    api_key</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">environ</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"WEC_API_KEY\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    timeout</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token number\" style=\"color:#36acaa\">30</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\">                             </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># ALWAYS set this — a slow backend hangs forever otherwise</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">classify</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">ticket</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    resp </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">chat</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">completions</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">create</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"Qwen2.5-3B-Instruct\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        name</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"ticket-classify\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\">             </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># names the trace in Langfuse</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        messages</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"user\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token string\" style=\"color:#e3116c\">\"Classify this support ticket. Return ONLY JSON with keys \"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token string\" style=\"color:#e3116c\">\"category (billing/technical/account/other), priority (low/medium/high), \"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"summary. Ticket: </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">ticket</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> resp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">choices</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> __name__ </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"__main__\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">classify</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"I was charged twice this month.\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    get_client</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">flush</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">                    </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># push traces before the process exits</span><br></div></code></pre></div></div>\n<p>Run it, then open <strong>Tracing</strong> — the call shows up in seconds with input, output, latency, token\ncounts, and (once we add pricing) cost.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"A single trace — input, output, latency, and token counts\" src=\"https://development-wec.wiline.com/docs/assets/images/langfuse-first-trace-f013e891fddc5d65b3525d5e1ad59fb7.png\" width=\"2532\" height=\"1438\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> One <code>ticket-classify</code> call in Langfuse: the prompt, the JSON output, latency (~1.2s),\nand token counts — captured with zero code wrapped around the request.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Set a timeout</div><div class=\"admonitionContent_BuS1\"><p>The OpenAI client has <strong>no default timeout</strong>. If the model is slow or the backend stalls, your\napp hangs indefinitely — no error, no trace. <code>timeout=30</code> turns that into a fast, visible failure.</p></div></div>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"from-one-call-to-a-real-pipeline--the-span-tree\">From one call to a real pipeline — the span tree<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#from-one-call-to-a-real-pipeline--the-span-tree\" class=\"hash-link\" aria-label=\"Direct link to From one call to a real pipeline — the span tree\" title=\"Direct link to From one call to a real pipeline — the span tree\" translate=\"no\">​</a></h3>\n<p>One call is easy to log; you don't need Langfuse for that. Tracing earns its keep on <strong>multi-step</strong>\nfeatures, where it shows the <strong>whole chain as one tree</strong> so you can see <em>which step</em> broke. Let's make\nthe feature realistic — <strong>classify the ticket, then draft a reply from that category</strong> (two WEC calls) —\nand group them into a single trace with the <code>@observe</code> decorator:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">pipeline.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langfuse </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> observe</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> get_client</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> app </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> classify              </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># reuse the traced WEC client + classifier</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">draft_reply</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">ticket</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> category</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    resp </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">chat</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">completions</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">create</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        model</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"Qwen2.5-3B-Instruct\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        name</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">\"draft-reply\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        messages</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token string\" style=\"color:#e3116c\">\"role\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"user\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"content\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">            </span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">f\"A customer sent this </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">category</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\"> ticket. Write a short, helpful reply.\\nTicket: </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">{</span><span class=\"token string-interpolation interpolation\">ticket</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:#393A34\">}</span><span class=\"token string-interpolation string\" style=\"color:#e3116c\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> resp</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">choices</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token number\" style=\"color:#36acaa\">0</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">content</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:#393A34\">@observe</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">                                     </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># groups everything below into ONE trace</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">handle_ticket</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">ticket</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">-</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> </span><span class=\"token builtin\">str</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    category </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> classify</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">ticket</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">                </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># child span 1</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    reply </span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\"> draft_reply</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">ticket</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> category</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\">      </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># child span 2</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">return</span><span class=\"token plain\"> reply</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> __name__ </span><span class=\"token operator\" style=\"color:#393A34\">==</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"__main__\"</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">print</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">handle_ticket</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"I was charged twice this month.\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    get_client</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">flush</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Open that trace and you get a <strong>tree</strong>: <code>handle_ticket</code> → <code>ticket-classify</code> → <code>draft-reply</code>, each\nchild with its own input, output, latency, and tokens. Now a failure has an <strong>address</strong> — you can\npoint at the exact step that broke instead of guessing (we use this in Step 5).</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can now <strong>see inside any LLM call in production</strong> — input, output, latency, tokens — without\nwriting a line of logging code.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--replay-your-part-3-dataset-through-it\">Step 3 — Replay your part-3 dataset through it<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#step-3--replay-your-part-3-dataset-through-it\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — Replay your part-3 dataset through it\" title=\"Direct link to Step 3 — Replay your part-3 dataset through it\" translate=\"no\">​</a></h2>\n<p>To get real signal (not one call), push your <strong>part-3 dataset</strong> through the classifier so the\ndashboard has traffic to analyze:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">replay.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> os</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langfuse</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">openai </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> openai</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> langfuse </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> get_client</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">from</span><span class=\"token plain\"> app </span><span class=\"token keyword\" style=\"color:#00009f\">import</span><span class=\"token plain\"> classify   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># reuse the traced function</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> line </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token builtin\">open</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">path</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">expanduser</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token string\" style=\"color:#e3116c\">\"~/tickets_dataset.jsonl\"</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"color:#00009f\">if</span><span class=\"token plain\"> line</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">strip</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">        classify</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">json</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">loads</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">line</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token string\" style=\"color:#e3116c\">\"ticket\"</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">get_client</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">.</span><span class=\"token plain\">flush</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<p>Now the <strong>Home dashboard</strong> shows traces over time, a latency distribution (p50/p95), tokens by\nmodel, and a cost total — the \"watch it in production\" view.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Langfuse dashboard populated with replayed traffic\" src=\"https://development-wec.wiline.com/docs/assets/images/langfuse-scores-dashboard-e46938c8695864fa98dc8afa0e488b3a.png\" width=\"2526\" height=\"1432\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> After replaying the part-3 dataset: traces over time, a latency distribution, tokens\nby model, and a cost total — the \"watch it in production\" view.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"make-cost-real-for-wec-models\">Make cost real for WEC models<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#make-cost-real-for-wec-models\" class=\"hash-link\" aria-label=\"Direct link to Make cost real for WEC models\" title=\"Direct link to Make cost real for WEC models\" translate=\"no\">​</a></h3>\n<p>Langfuse auto-computes cost for models it knows (OpenAI, Anthropic), but the WEC Inference API\nreturns no dollar cost and Langfuse doesn't know WEC's model prices — so cost shows <code>$0</code>. This is\nthe same gap we hit in <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\">part 1</a>. Fix it once:\n<strong>Settings → Models → add a model</strong> matching <code>Qwen2.5-3B-Instruct</code> with your per-token input/output\nprices. Now every trace and the dashboard show real spend — the cost-tracking that Promptfoo\ncouldn't give us.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--auto-score-live-traffic-with-an-llm-judge\">Step 4 — Auto-score live traffic with an LLM judge<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#step-4--auto-score-live-traffic-with-an-llm-judge\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Auto-score live traffic with an LLM judge\" title=\"Direct link to Step 4 — Auto-score live traffic with an LLM judge\" translate=\"no\">​</a></h2>\n<p>CI eval checks a fixed set. Here we score <strong>every real call</strong> automatically with an LLM-as-judge.\nThis step looks like pure configuration — it's also where we hit, and solved, the best bug of the\nseries.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"wire-up-the-judge\">Wire up the judge<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#wire-up-the-judge\" class=\"hash-link\" aria-label=\"Direct link to Wire up the judge\" title=\"Direct link to Wire up the judge\" translate=\"no\">​</a></h3>\n<p>Create the evaluator via <strong>Evaluators → + New evaluator</strong>. It's a 3-step wizard:</p>\n<p><strong>1. Select Evaluator</strong> — pick the <strong>Correctness</strong> LLM-as-judge template.</p>\n<p><strong>2. Set up LLM connection</strong> — the judge needs a model to call, and there's none yet, so click\n<strong>+ Add LLM Connection</strong>. The dialog has more fields than you'd expect — here's exactly what each\none needs:</p>\n<ul>\n<li class=\"\"><strong>LLM adapter:</strong> <code>openai</code></li>\n<li class=\"\"><strong>Provider name:</strong> any label <em>without colons</em> (e.g. <code>wec-judge</code>) — just the connection's name in Langfuse</li>\n<li class=\"\"><strong>API Key:</strong> your WEC key</li>\n<li class=\"\">Click <strong>Show advanced settings</strong>:<!-- -->\n<ul>\n<li class=\"\"><strong>API Base URL:</strong> <code>https://inference.wiline.com/v1</code> — <strong>critical</strong>; left blank it hits real OpenAI</li>\n<li class=\"\"><strong>Use Responses API:</strong> leave <strong>off</strong> (WEC speaks Chat Completions)</li>\n<li class=\"\"><strong>Extra Headers:</strong> none — skip</li>\n<li class=\"\"><strong>Enable default models:</strong> turn <strong>off</strong> (you don't want OpenAI's model list here)</li>\n<li class=\"\"><strong>Custom models → Add custom model name:</strong> <code>Qwen3.5:9B</code> — the model choice matters a lot\nhere, as you're about to see</li>\n</ul>\n</li>\n<li class=\"\"><strong>Create connection</strong>, then select provider <strong><code>wec-judge</code></strong> and model <strong><code>Qwen3.5:9B</code></strong> → <strong>Save</strong>.</li>\n</ul>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The New LLM Connection dialog — WEC base URL, default models off, a custom WEC model added\" src=\"https://development-wec.wiline.com/docs/assets/images/langfuse-llm-connection-e83545064c144db968575c7d85d0c809.png\" width=\"2520\" height=\"1274\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 4.</strong> The New LLM Connection dialog: adapter <code>openai</code>, the WEC API Base URL, default\nmodels switched off, and a custom model added — this is what points the judge at WEC instead of\nreal OpenAI.</p>\n<p><strong>3. Run Evaluator</strong> — add a filter <strong>Name = <code>ticket-classify</code></strong>, toggle <strong>Run on live incoming\nobservations</strong> so it scores new traffic, and <strong>Execute</strong>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The LLM-as-judge evaluator configuration\" src=\"https://development-wec.wiline.com/docs/assets/images/langfuse-evaluator-config-328cb01c6a061467eca223c1d7266d31.png\" width=\"2526\" height=\"1436\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 5.</strong> The Correctness evaluator — score name, the <code>ticket-classify</code> filter, and \"run on\nlive incoming observations.\"</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"then-no-scores-debugging-the-120-second-timeout\">Then: no scores. Debugging the 120-second timeout<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#then-no-scores-debugging-the-120-second-timeout\" class=\"hash-link\" aria-label=\"Direct link to Then: no scores. Debugging the 120-second timeout\" title=\"Direct link to Then: no scores. Debugging the 120-second timeout\" translate=\"no\">​</a></h3>\n<p>We configured everything, sent fresh traffic… and nothing. No score on any trace. Here's how we\ntracked it down — you may hit the same wall:</p>\n<ol>\n<li class=\"\"><strong>Check the evaluator's log.</strong> <strong>Evaluators → your evaluator → Logs</strong> had the real error:\n<code>Request timed out after 120000ms</code>. Every run, exactly 120s — so the judge <em>was</em> firing, its\nLLM call just never came back.</li>\n<li class=\"\"><strong>Suspect the model.</strong> Our first judge, <code>Qwen2.5-3B-Instruct</code>, rejected tool calls outright\n(instant 400 — a missing vLLM config on the backend; more on that below). We switched to\n<code>Qwen3.5:9B</code>, which answers a direct tool call in ~3s. <strong>Still timed out.</strong></li>\n<li class=\"\"><strong>Suspect the network.</strong> <code>curl</code> from the host: 200 in half a second. The same judge-style\nrequest from <em>inside the worker container</em>: ~3s. Model, network, container — all fine.</li>\n<li class=\"\"><strong>Reproduce what Langfuse actually sends.</strong> The judge doesn't call the API like our curl — it\nuses <em>structured output</em> (<code>response_format</code>) so the score comes back machine-readable. Replaying\nthat exact call shape exposed the problem: on this backend, <strong>judge-style requests to the\nreasoning model are wildly slow and unpredictable</strong> — the identical request ranged from 3 to\n95 seconds across runs (the model \"thinks\" at length, and JSON mode adds overhead). With the\njudge's longer real prompt, runs blew past the 120s default. <strong>Nothing was broken — it was just\nslower than the timeout.</strong></li>\n</ol>\n<p>The fix is two lines of configuration. Langfuse reads its LLM timeout from\n<code>LANGFUSE_FETCH_LLM_COMPLETION_TIMEOUT_MS</code> (default 120000). Give it room:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">docker-compose.override.yml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">services</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">langfuse-worker</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">environment</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">LANGFUSE_FETCH_LLM_COMPLETION_TIMEOUT_MS</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"600000\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">langfuse-web</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">environment</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token key atrule\" style=\"color:#00a4db\">LANGFUSE_FETCH_LLM_COMPLETION_TIMEOUT_MS</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"600000\"</span><br></div></code></pre></div></div>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> langfuse-worker langfuse-web</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># verify it landed inside the container:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> langfuse-worker </span><span class=\"token function\" style=\"color:#d73a49\">env</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> TIMEOUT</span><br></div></code></pre></div></div>\n<p>Send a fresh trace through <code>app.py</code>, wait a couple of minutes, and:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Correctness score attached to a real trace\" src=\"https://development-wec.wiline.com/docs/assets/images/langfuse-score-on-trace-99653e359fac7e126dc097103602a4e5.png\" width=\"2520\" height=\"1424\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 6.</strong> There it is — <strong>Correctness: 1.00</strong> attached to a live <code>ticket-classify</code> trace, with\nthe judge's reasoning a click away. Every new call now gets scored automatically.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can now <strong>auto-grade real production traffic</strong> — and debug an evaluator that silently produces\nnothing: check its Logs, isolate model vs. network vs. container, match the timeout to reality.</p></div></div>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Two constraints to know about</div><div class=\"admonitionContent_BuS1\"><p><strong>The judge needs tool calling.</strong> If your model returns a 400 on any <code>tools</code> request, that's\nusually the serving config, not the model — on vLLM it's the <code>--enable-auto-tool-choice</code> and\n<code>--tool-call-parser</code> flags. That was our case: we reported it, the platform team enabled the\nflags on <code>Qwen2.5-3B-Instruct</code> the same day, and the 400 disappeared. Only the judge's connection\nis affected — your app keeps its model.</p><p><strong>Scoring is slow (~2 min/score) and that's the backend, not you.</strong> The reasoning model \"thinks\"\nat length on judge prompts — worth reporting to your platform team (reasoning budget caps, or a\nguided-decoding backend like xgrammar on vLLM). The raised timeout keeps scoring reliable meanwhile.</p></div></div>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Tempting shortcut that doesn't work: a small model as judge</div><div class=\"admonitionContent_BuS1\"><p>Once tool calling was enabled on <code>Qwen2.5-3B</code>, we tried it as the judge — scores came back in\n<strong>2–10 seconds</strong> instead of ~2 minutes. But they were garbage: the same kind of correct answer got\n<strong>1, 1, 0, and 0.05</strong> across four runs, with near-identical rationales attached to opposite\nscores. The small model fills the score format perfectly; it just can't actually <em>evaluate</em>.\nMirror image of <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/\">part 3</a>'s finding: the small\nmodel wins at <em>classifying</em> and loses at <em>judging</em> — judging is a reasoning task. Keep the judge\non <code>Qwen3.5:9B</code>: slow but coherent.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--find-the-failure-your-tests-missed\">Step 5 — Find the failure your tests missed<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#step-5--find-the-failure-your-tests-missed\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — Find the failure your tests missed\" title=\"Direct link to Step 5 — Find the failure your tests missed\" translate=\"no\">​</a></h2>\n<p>This is where the <strong>span tree</strong> pays off. Open a <code>handle_ticket</code> trace and you don't get one opaque\nblob — you get the <strong>whole chain as a tree</strong>, each step with its own input, output, latency, and\ntokens:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Drilling into a trace — the span tree localizes each step\" src=\"https://development-wec.wiline.com/docs/assets/images/langfuse-failing-trace-8fcb77412fc10f743dad85e747382695.png\" width=\"2530\" height=\"1432\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 7.</strong> The <code>handle_ticket</code> tree — <code>handle_ticket → ticket-classify → draft-reply</code> — with\nper-step latency and tokens. Every step has an address you can inspect.</p>\n<p>That structure is what makes production failures debuggable. Say a customer gets a nonsense reply.\nWithout tracing you'd only know \"the reply was bad.\" With the tree you expand it and read each step\n— for example:</p>\n<ul>\n<li class=\"\"><strong><code>ticket-classify</code></strong> → <code>category: \"billing\"</code> ✅ correct — so the classifier isn't the problem.</li>\n<li class=\"\"><strong><code>draft-reply</code></strong> → invented a refund policy that doesn't exist ❌ — <strong>the bug is in step 2.</strong></li>\n</ul>\n<p>You've localized the failure to the <strong>exact step, input, and output</strong> — you'd fix the <code>draft-reply</code>\nprompt, not waste time on the classifier. Offline eval tells you the pass rate dropped; the trace\ntells you <em>which call, on which input, produced which wrong output, at which step.</em> And now that\nStep 4 scores every call, you don't even have to hunt: <strong>filter Tracing by low score</strong> and the\nsuspicious traces surface themselves — often a phrasing your part-3 dataset never covered, which is\nexactly what you capture back next.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can now <strong>pin a production failure to the exact step, input, and output</strong> — no more guessing\nwhich call invented that refund policy.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-6--close-the-loop-production--better-evals\">Step 6 — Close the loop: production → better evals<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#step-6--close-the-loop-production--better-evals\" class=\"hash-link\" aria-label=\"Direct link to Step 6 — Close the loop: production → better evals\" title=\"Direct link to Step 6 — Close the loop: production → better evals\" translate=\"no\">​</a></h2>\n<p>A production failure is a <strong>missing test case</strong> — and Langfuse makes capturing it one click. Create\nthe dataset once (<strong>Datasets → + New dataset</strong>, e.g. <code>production-failures</code>), then on any trace click\n<strong>+ Add to datasets</strong> and pick it. The trace's input and output are prefilled as a new item:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Adding a trace back into the eval dataset\" src=\"https://development-wec.wiline.com/docs/assets/images/langfuse-dataset-loop-9e09a4627451a945e4df5245e6018fe7.png\" width=\"2522\" height=\"1430\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 8.</strong> \"Add to datasets\" turns a production trace into a new test case — input and expected\noutput prefilled — feeding it straight back into your part-3 dataset.</p>\n<p>To automate it, pull the traces you've bookmarked or added to a dataset via the Langfuse SDK, write\n<code>{ticket, category, priority}</code> rows into <code>tickets_dataset.jsonl</code>, and re-run the part-1/part-3\nPromptfoo eval — now it covers the real-world case.</p>\n<p>Now your CI eval is stronger <em>because</em> production found a hole in it. That's the flywheel:\n<strong>offline eval → ship → observe &amp; score real traffic → capture failures → offline eval gets\nstronger.</strong></p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You've closed the loop: <strong>production failures now make your evals stronger</strong> — the flywheel that\nkeeps an AI feature good after it ships.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-you-can-now-do\">What you can now do<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#what-you-can-now-do\" class=\"hash-link\" aria-label=\"Direct link to What you can now do\" title=\"Direct link to What you can now do\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>See inside</strong> every WEC call in production (input/output/latency/cost).</li>\n<li class=\"\"><strong>Score real traffic automatically</strong> with an LLM judge — and debug the evaluator itself when it\ngoes quiet.</li>\n<li class=\"\"><strong>Debug a specific failure</strong> down to the exact step, not just an aggregate metric.</li>\n<li class=\"\"><strong>Turn production failures into test cases</strong>, so your evals keep improving.</li>\n</ul>\n<p>That's the \"keep it good after shipping\" rung. Offline eval (parts 1–3) proves it works before\nlaunch; this proves — and keeps — it working after.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting\">Troubleshooting<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#troubleshooting\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting\" title=\"Direct link to Troubleshooting\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"address-already-in-use-on-docker-compose-up\"><a id=\"ts-ports\"></a>\"address already in use\" on <code>docker compose up</code><a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#address-already-in-use-on-docker-compose-up\" class=\"hash-link\" aria-label=\"Direct link to address-already-in-use-on-docker-compose-up\" title=\"Direct link to address-already-in-use-on-docker-compose-up\" translate=\"no\">​</a></h3>\n<p>Another service holds port 3000 (web) or 5432 (Postgres). The databases don't need host ports —\nin <code>docker-compose.yml</code>, remap <code>langfuse-web</code> to a free port (<code>3001:3000</code>) and remove the Postgres\nhost mapping. Compose <em>merges</em> <code>ports</code> lists, so an override file with <code>ports: []</code> won't remove\nthem; edit the compose directly.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-web-container-wont-start\">The web container won't start<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#the-web-container-wont-start\" class=\"hash-link\" aria-label=\"Direct link to The web container won't start\" title=\"Direct link to The web container won't start\" translate=\"no\">​</a></h3>\n<p>Almost always <code>ENCRYPTION_KEY</code> — it must be <code>openssl rand -hex 32</code> (64 hex chars), not base64.\nCheck <code>docker compose logs langfuse-web</code>.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"cost-shows-0\">Cost shows <code>$0</code><a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#cost-shows-0\" class=\"hash-link\" aria-label=\"Direct link to cost-shows-0\" title=\"Direct link to cost-shows-0\" translate=\"no\">​</a></h3>\n<p>Langfuse has no price for WEC's models by default. Add the model + per-token prices under\n<strong>Settings → Models</strong> (Step 3).</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"no-scores-appear-on-new-traces\">No scores appear on new traces<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#no-scores-appear-on-new-traces\" class=\"hash-link\" aria-label=\"Direct link to No scores appear on new traces\" title=\"Direct link to No scores appear on new traces\" translate=\"no\">​</a></h3>\n<p>Check <strong>Evaluators → your evaluator → Logs</strong> first — an error there tells you it's running and\nfailing, not idle. Then match the error:</p>\n<ul>\n<li class=\"\"><strong>A 400 mentioning tool calls / <code>tool_choice</code></strong> — tool calling isn't enabled for that model on\nthe serving backend (on vLLM: the <code>--enable-auto-tool-choice</code> / <code>--tool-call-parser</code> flags).\nTwo ways out: point the judge's LLM Connection at a model where it works (<code>Qwen3.5:9B</code>), or ask\nyour platform team to enable the flags — that's what fixed <code>Qwen2.5-3B-Instruct</code> for us. Either\nway, only the judge changes — your app keeps its model.</li>\n<li class=\"\"><strong><code>Request timed out after 120000ms</code></strong> — the judge's structured-output call is slower than\nLangfuse's default timeout. Raise <code>LANGFUSE_FETCH_LLM_COMPLETION_TIMEOUT_MS</code> (Step 4) and\nconfirm with <code>docker compose exec langfuse-worker env | grep TIMEOUT</code>. If plain <code>curl</code> is fast\nbut scoring still times out, don't chase the network — the structured-output path is what's\nslow, and that's a backend fix.</li>\n</ul>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"scores-take-2-minutes-each\">Scores take ~2 minutes each<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#scores-take-2-minutes-each\" class=\"hash-link\" aria-label=\"Direct link to Scores take ~2 minutes each\" title=\"Direct link to Scores take ~2 minutes each\" translate=\"no\">​</a></h3>\n<p>That's not a hang — it's the reasoning model generating long hidden chain-of-thought on judge\nprompts (highly variable: the same request can take 3s or 95s), plus structured-output overhead.\nRaising the timeout makes scoring <em>reliable</em> but not <em>fast</em> — the real fix is platform-side\n(reasoning budget caps, guided-decoding backend).</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Observe &amp; score production</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>You can now build, prove, observe, and improve a single WEC feature. The last rung is the real\napp pattern: <strong>RAG</strong> — retrieval + generation — with everything from parts 1–4 baked in\n(schema-checked output, an eval dataset, and Langfuse tracing over the whole pipeline). That's\nwhere it all comes together.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"further-reading\">Further reading<a href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/#further-reading\" class=\"hash-link\" aria-label=\"Direct link to Further reading\" title=\"Direct link to Further reading\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://langfuse.com/self-hosting\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Langfuse — self-hosting docs</a></li>\n<li class=\"\"><a href=\"https://langfuse.com/faq/all/best-phoenix-arize-alternatives\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Langfuse vs Arize Phoenix — trade-offs</a></li>\n<li class=\"\"><a href=\"https://posthog.com/blog/best-open-source-llm-observability-tools\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Open-source LLM observability tools compared (PostHog)</a></li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/",
            "title": "Catch what your tests miss: observe and score your WEC app in production with Langfuse",
            "summary": "CI evals pass on a fixed test set — but production sends inputs you never tested. Self-host Langfuse on a WEC Instance, trace every real call, auto-score live traffic with an LLM judge, drill into the exact step that broke, and feed failures back to make your evals stronger. Every command — and every dead end — is real.",
            "date_modified": "2026-07-07T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "observability",
                "langfuse",
                "evals",
                "inference",
                "production",
                "self-hosting"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-avatar-31b32c657d72dac0a7edbd77c24ec717.jpg\" alt=\"Promptfoo\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"data:image/png;base64,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\" alt=\"JSON\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 8 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->8</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->8<!-- --> earned</span></div><div class=\"skillTracker__series\">AI evals &amp; observability</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove a model works</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trustworthy JSON</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">3</span><span class=\"skillTracker__skill\" data-state=\"current\">Real test data at scale</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Observe &amp; score production</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">RAG, end to end</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">Catch regressions in CI</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trace &amp; debug agent tool calls</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/web-search-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Add live web search</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Your eval passes 100% — of the <strong>two test cases you typed by hand.</strong> Real users won't phrase\nthings the way you did.</p>\n<p>In <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\">part 1</a> and\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/\">part 2</a> we built a real eval harness —\nassertions, schema validation, a model matrix. But two cases can't tell you if your app works;\nthey can only tell you it didn't crash on two inputs.</p>\n<p>This guide fixes that. The skill is <strong>producing a dataset you can actually trust</strong>: we use the\n<strong>WEC Inference API</strong> to <strong>generate labeled tickets</strong>, then <strong>validate and curate them</strong> — because\ngenerated labels are <em>not</em> automatically correct, and treating them as gospel just moves the bug.\nThe result is real test data at scale. When you finally run it through the part-2 harness, coverage\nsurfaces the failures — genuine misclassifications <em>and</em> debatable labels — that two hand-picked\ncases hide.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"how-it-fits-together\">How it fits together<a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#how-it-fits-together\" class=\"hash-link\" aria-label=\"Direct link to How it fits together\" title=\"Direct link to How it fits together\" translate=\"no\">​</a></h2>\n<p>Each step feeds the next, ending in a dataset that plugs straight into the part-2 eval:</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">\n<p>A <strong>WEC Inference API key</strong> — see <a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/inference/api_keys/\">Inference → API Keys</a>.</p>\n</li>\n<li class=\"\">\n<p><strong><code>jq</code></strong>, <strong><code>curl</code></strong>, and <strong>Node.js 22+</strong> (Promptfoo runs via <code>npx</code>).</p>\n</li>\n<li class=\"\">\n<p>Your key exported and sanitized (a pasted key often carries invisible characters):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">WEC_API_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">'sk-your-key'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">WEC_API_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable builtin class-name\" style=\"color:#36acaa\">printf</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'%s'</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">\"</span><span class=\"token variable string variable\" style=\"color:#36acaa\">$WEC_API_KEY</span><span class=\"token variable string\" style=\"color:#e3116c\">\"</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable assign-left variable environment constant\" style=\"color:#36acaa\">LC_ALL</span><span class=\"token variable operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">C </span><span class=\"token variable function\" style=\"color:#d73a49\">tr</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-cd</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'[:print:]'</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><br></div></code></pre></div></div>\n</li>\n</ul>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>If a call returns a <code>401</code> / \"invalid API key\", your key is expired or revoked — generate a\nfresh one in <strong>Inference → API Keys</strong>. See <a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#ts-401\" class=\"\">Troubleshooting</a>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--generate-one-labeled-batch\">Step 1 — Generate one labeled batch<a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#step-1--generate-one-labeled-batch\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Generate one labeled batch\" title=\"Direct link to Step 1 — Generate one labeled batch\" translate=\"no\">​</a></h2>\n<p>Ask the model for tickets <strong>with their answers</strong> — the labels are what make it a <em>dataset</em>, not\njust inputs. We reuse the exact schema from part 2 (<code>category</code> + <code>priority</code> enums):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> https://inference.wiline.com/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$WEC_API_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"model\": \"Qwen2.5-3B-Instruct\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"temperature\": 0.7,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"messages\": [{\"role\":\"user\",\"content\":\"Generate 8 diverse, realistic customer support tickets for a cloud hosting company. Return ONLY JSONL (one JSON object per line), each with keys: ticket (string), category (one of: billing, technical, account, other), priority (one of: low, medium, high). No markdown, no fences.\"}]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  }'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.choices[0].message.content'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">{\"ticket\":\"Our server went down unexpectedly and we're not sure what caused it.\",\"category\":\"technical\",\"priority\":\"high\"}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">{\"ticket\":\"We need to upgrade our plan but the system is showing an error message when trying to change plans.\",\"category\":\"account\",\"priority\":\"medium\"}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">{\"ticket\":\"We're having trouble accessing our database and can't seem to log in.\",\"category\":\"technical\",\"priority\":\"high\"}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">... 5 more lines ...</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Step 1 — one call returns clean, labeled JSONL\" src=\"https://development-wec.wiline.com/docs/assets/images/dataset-gen-batch-adc9c63dcd4af4c2991e7581ea5908ec.png\" width=\"2524\" height=\"468\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> A single generation: 8 diverse tickets, each already carrying its <code>category</code> and <code>priority</code> label.</p>\n<p>Clean JSONL, valid labels, real diversity — and it came back in seconds. We deliberately picked a\n<strong>small, non-reasoning model</strong> (<code>Qwen2.5-3B-Instruct</code>) for generation. Here's why that matters.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Why not a reasoning model for bulk generation?</div><div class=\"admonitionContent_BuS1\"><p>Reasoning models return their chain-of-thought in a separate <strong><code>reasoning_content</code></strong> field — tokens\nyou pay for but never use. Ask a reasoning model like <code>Qwen3.5:9B</code> for a few tickets and compare the\nhidden reasoning against the actual answer:</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> https://inference.wiline.com/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$WEC_API_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"Qwen3.5:9B\",\"temperature\":0.7,\"messages\":[{\"role\":\"user\",\"content\":\"Generate 3 realistic customer support tickets for a cloud hosting company. Return ONLY JSONL, keys: ticket, category, priority. No markdown, no fences.\"}]}'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token string\" style=\"color:#e3116c\">'{model, completion_tokens: .usage.completion_tokens, reasoning_chars: (.choices[0].message.reasoning_content|length), answer_chars: (.choices[0].message.content|length)}'</span><br></div></code></pre></div></div><div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"model\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Qwen3.5:9B\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"completion_tokens\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">2000</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"reasoning_chars\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">7116</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token property\" style=\"color:#36acaa\">\"answer_chars\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">412</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div><p><span class=\"zoomImage__wrap\"><img alt=\"A reasoning model&amp;#39;s output — hidden reasoning dwarfs the answer\" src=\"https://development-wec.wiline.com/docs/assets/images/dataset-gen-usage-5ee23a331ab19ce10dfddd509162743d.png\" width=\"2536\" height=\"370\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1b.</strong> For just 3 tickets, <code>Qwen3.5:9B</code> wrote <strong>7,116 characters of hidden reasoning to\nproduce 412 of answer</strong> — you pay for all ~2,000 completion tokens. (Counts vary per call; the\nlopsidedness doesn't.) That's why we generate with the small, non-reasoning <code>Qwen2.5-3B</code> — and it's\nthe same reason those reasoning models stumble on structured output in Step 5's matrix.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--validate-before-you-trust-it\">Step 2 — Validate before you trust it<a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#step-2--validate-before-you-trust-it\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Validate before you trust it\" title=\"Direct link to Step 2 — Validate before you trust it\" translate=\"no\">​</a></h2>\n<p>Never treat generated data as ground truth without checking it. Save a batch and validate three\nthings: every line parses, every label is in-enum, and the classes are reasonably balanced.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> https://inference.wiline.com/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$WEC_API_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"Qwen2.5-3B-Instruct\",\"temperature\":0.7,\"messages\":[{\"role\":\"user\",\"content\":\"Generate 8 diverse, realistic customer support tickets for a cloud hosting company. Return ONLY JSONL (one JSON object per line), each with keys: ticket (string), category (one of: billing, technical, account, other), priority (one of: low, medium, high). No markdown, no fences.\"}]}'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.choices[0].message.content'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> tickets.jsonl</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 1) do all lines parse as JSON?</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-e</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">.</span><span class=\"token plain\"> tickets.jsonl </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> /dev/null </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"all lines valid JSON\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">||</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"INVALID JSON present\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 2) any out-of-enum labels? (no rows printed = all valid)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'select((.category|IN(\"billing\",\"technical\",\"account\",\"other\")|not) or (.priority|IN(\"low\",\"medium\",\"high\")|not))'</span><span class=\"token plain\"> tickets.jsonl</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 3) class distribution</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-rs</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'group_by(.category)[] | \"\\(.[0].category): \\(length)\"'</span><span class=\"token plain\"> tickets.jsonl</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">all lines valid JSON</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">account: 2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">billing: 2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">other: 1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">technical: 3</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Step 2 — validating the batch: valid JSON, in-enum labels, class distribution\" src=\"https://development-wec.wiline.com/docs/assets/images/dataset-validate-9b1201646866c2062c5e9aa3d093cfd2.png\" width=\"2534\" height=\"434\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> Validation output — every line parses, no out-of-enum labels, and a reasonable class spread.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>jq gotcha</div><div class=\"admonitionContent_BuS1\"><p>The intuitive <code>[\"billing\",...] | index(.category)</code> <strong>fails</strong> with <em>\"Cannot index array with\nstring\"</em> — inside the pipe, <code>.</code> is the array, so <code>.category</code> tries to index it. Use\n<strong><code>.category | IN(\"billing\", ...)</code></strong> instead. See <a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#ts-jq\" class=\"\">Troubleshooting</a>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--scale-up-and-dedupe\">Step 3 — Scale up and dedupe<a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#step-3--scale-up-and-dedupe\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — Scale up and dedupe\" title=\"Direct link to Step 3 — Scale up and dedupe\" translate=\"no\">​</a></h2>\n<p>One batch of 8 isn't enough. Loop the call a few times (higher temperature for variety across\nbatches), concatenate, then dedupe by ticket text:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">for</span><span class=\"token plain\"> </span><span class=\"token for-or-select variable\" style=\"color:#36acaa\">i</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">in</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable function\" style=\"color:#d73a49\">seq</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable number\" style=\"color:#36acaa\">1</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable number\" style=\"color:#36acaa\">5</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"color:#00009f\">do</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> https://inference.wiline.com/v1/chat/completions </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$WEC_API_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Content-Type: application/json\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'{\"model\":\"Qwen2.5-3B-Instruct\",\"temperature\":0.9,\"messages\":[{\"role\":\"user\",\"content\":\"Generate 8 diverse, realistic customer support tickets for a cloud hosting company. Return ONLY JSONL (one JSON object per line), each with keys: ticket (string), category (one of: billing, technical, account, other), priority (one of: low, medium, high). No markdown, no fences.\"}]}'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.choices[0].message.content // empty'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"batch </span><span class=\"token string variable\" style=\"color:#36acaa\">$i</span><span class=\"token string\" style=\"color:#e3116c\"> done\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;2</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">done</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> tickets_raw.jsonl</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># dedupe by ticket text</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sc</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'unique_by(.ticket)[]'</span><span class=\"token plain\"> tickets_raw.jsonl </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> tickets_dataset.jsonl</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"raw: </span><span class=\"token string variable\" style=\"color:#36acaa\">$(</span><span class=\"token string variable function\" style=\"color:#d73a49\">wc</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-l</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable operator\" style=\"color:#393A34\">&lt;</span><span class=\"token string variable\" style=\"color:#36acaa\"> tickets_raw.jsonl</span><span class=\"token string variable\" style=\"color:#36acaa\">)</span><span class=\"token string\" style=\"color:#e3116c\">  →  deduped: </span><span class=\"token string variable\" style=\"color:#36acaa\">$(</span><span class=\"token string variable function\" style=\"color:#d73a49\">wc</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-l</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable operator\" style=\"color:#393A34\">&lt;</span><span class=\"token string variable\" style=\"color:#36acaa\"> tickets_dataset.jsonl</span><span class=\"token string variable\" style=\"color:#36acaa\">)</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">raw: 40  →  deduped: 40</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Step 3 — five batches concatenated and deduped to 40 unique rows\" src=\"https://development-wec.wiline.com/docs/assets/images/dataset-scale-dedupe-74ac55864b3da488aa334816e11ede4a.png\" width=\"2524\" height=\"636\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> Scaling up: 5 batches → 40 rows after exact-duplicate removal.</p>\n<p>Zero exact duplicates across 5 batches — a good sign the model isn't just repeating itself.</p>\n<div class=\"theme-admonition theme-admonition-caution admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>caution</div><div class=\"admonitionContent_BuS1\"><p><code>unique_by(.ticket)</code> only catches <strong>identical</strong> text. Two differently-worded \"server is down\"\ntickets are <em>semantic</em> near-duplicates and will slip through — catching those needs\nembedding-based clustering, a more advanced step. For a starter dataset, exact dedup is fine.</p></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Guard against silent failures</div><div class=\"admonitionContent_BuS1\"><p>Note the <strong><code>// empty</code></strong> in the <code>jq</code> filter. If a call fails (revoked key, model timeout), the\nresponse has no content and <code>.choices[0].message.content</code> is <code>null</code> — a plain <code>jq -r</code> would then\nwrite the literal word <code>null</code> into your dataset, silently poisoning it. <code>// empty</code> drops those,\nso a failed call adds <strong>nothing</strong> instead of a junk row. Always check the row count afterward.\nSee <a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#ts-null\" class=\"\">Troubleshooting</a>.</p></div></div>\n<p>Validate the full 40 the same way as Step 2 (<code>jq -e .</code>, the enum check, and both distributions).\nOurs came out balanced: technical 15 · account 10 · billing 10 · other 5; priority high 13 ·\nmedium 14 · low 13.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You now have a <strong>repeatable pipeline for real test data</strong> — 40 validated, deduped, labeled rows\nwhere you used to have two.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--convert-to-a-promptfoo-dataset\">Step 4 — Convert to a Promptfoo dataset<a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#step-4--convert-to-a-promptfoo-dataset\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Convert to a Promptfoo dataset\" title=\"Direct link to Step 4 — Convert to a Promptfoo dataset\" translate=\"no\">​</a></h2>\n<p>Promptfoo reads test cases from a CSV: each column becomes a variable, and the special\n<strong><code>__expected</code></strong> column holds a per-row assertion. We map <code>ticket</code> → the prompt var and\n<code>__expected</code> → <code>icontains:&lt;the correct category&gt;</code>. <code>jq @csv</code> handles the commas and quotes\ninside ticket text:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'ticket,__expected'</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'[.ticket, (\"icontains:\" + .category)] | @csv'</span><span class=\"token plain\"> tickets_dataset.jsonl</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> tests.csv</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">wc</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-l</span><span class=\"token plain\"> tests.csv          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 41 = header + 40 rows</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--put-the-dataset-to-work\">Step 5 — Put the dataset to work<a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#step-5--put-the-dataset-to-work\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — Put the dataset to work\" title=\"Direct link to Step 5 — Put the dataset to work\" translate=\"no\">​</a></h2>\n<p>The dataset is the deliverable — everything from here is what it <em>unlocks</em>. First payoff: a model\ncomparison you can actually believe. On two hand-typed cases a ranking is noise; on 40 real,\nlabeled rows it's a measurement. Add each WEC model as a provider and run the whole set:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">description: \"Ticket classifier — model matrix on the synthetic 40-row dataset\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">providers:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0 }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:Qwen3.5:9B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0 }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:Qwen3.5-122B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0 }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">prompts:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - |</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    Classify this support ticket. Return ONLY JSON with keys:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"category\" (one of: billing, technical, account, other),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"priority\" (one of: low, medium, high),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"summary\" (string, max 12 words).</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    Ticket: {{ticket}}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">defaultTest:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  options:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    transform: |</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      const m = output.match(/\\{[\\s\\S]*?\\}/);</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      return m ? m[0] : output;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  assert:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    - type: is-json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      value:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        type: object</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        required: [category, priority, summary]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        properties:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          category: { type: string, enum: [billing, technical, account, other] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          priority: { type: string, enum: [low, medium, high] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          summary: { type: string }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">tests: file://tests.csv</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml</span><br></div></code></pre></div></div>\n<p>The result is the opposite of what \"bigger is better\" would predict:</p>\n<table><thead><tr><th>Model</th><th>Accuracy</th><th>Completion tokens / ticket</th></tr></thead><tbody><tr><td><strong><code>Qwen2.5-3B-Instruct</code></strong> (non-reasoning)</td><td><strong>85%</strong> (34/40)</td><td>~30</td></tr><tr><td><code>Qwen3.5:9B</code> (reasoning)</td><td>47.5% (19/40)</td><td>~965</td></tr><tr><td><code>Qwen3.5-122B</code> (reasoning)</td><td>15% (6/40)</td><td>~1,013</td></tr></tbody></table>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Step 5 — the model matrix over the 40-row dataset\" src=\"https://development-wec.wiline.com/docs/assets/images/dataset-eval-matrix-9ffe20345f2040803fce117f6803a6ff.png\" width=\"2406\" height=\"1414\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 4.</strong> Same dataset, three models — the small non-reasoning model wins by a mile.</p>\n<p><strong>The small, non-reasoning model wins decisively — and the bigger the reasoning model, the <em>worse</em>\nit does.</strong> The reasoning models aren't dumber at <em>classifying</em>; they <strong>bury the JSON in their\nchain-of-thought</strong>. <code>Qwen3.5:9B</code> leaks <code>Thinking: …</code> into the output; <code>Qwen3.5-122B</code> often returns\nan <strong>empty answer entirely</strong> — all ~1,000 tokens spent reasoning, nothing parseable left. They also\nburn <strong>~30× more tokens per ticket</strong>, which is why the full matrix took <strong>12m 31s</strong>, almost all of\nit spent waiting on the two big models. <strong>For structured output, thinking out loud is a liability,\nnot an advantage</strong> — a counterintuitive finding you'd never get from a leaderboard.</p>\n<p>But notice what actually made that finding possible: <strong>the dataset.</strong> On two hand-typed cases those\npercentages would be coin-flips; 40 labeled rows are what turn \"which model?\" from a guess into a\nmeasurement. The comparison is the <em>reward</em> for building the data — not a substitute for it.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can now <strong>pick a model with evidence instead of vibes</strong> — something no leaderboard can do for\nyou, because it doesn't know your task.</p></div></div>\n<p>(The <code>transform</code> grabs the first <code>{…}</code> block with a <strong>non-greedy</strong> <code>*?</code> match — cheap insurance for\nmodels that wrap JSON in prose, exactly like <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/\">part 2</a>.)</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>We score <code>category</code> here — <code>priority</code> is your next assertion</div><div class=\"admonitionContent_BuS1\"><p>The <code>__expected</code> column grades only the predicted <strong>category</strong> against the label. The dataset also\ncarries a <code>priority</code> label we don't score yet. To grade both fields at once, add a <code>javascript</code>\nassertion that parses the JSON output and compares <code>priority</code> too — the exact same pattern, one more\ncheck. Scoring more of what you generated is the cheapest way to make an eval stricter.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-6--dig-into-the-winners-failures\">Step 6 — Dig into the winner's failures<a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#step-6--dig-into-the-winners-failures\" class=\"hash-link\" aria-label=\"Direct link to Step 6 — Dig into the winner's failures\" title=\"Direct link to Step 6 — Dig into the winner's failures\" translate=\"no\">​</a></h2>\n<p>Take the winner — <code>Qwen2.5-3B</code> at <strong>85% (34/40)</strong> — and pull its 6 failing tickets, comparing each\nto its label. (<code>__expected</code> isn't stored as a variable — Promptfoo consumes it as the assertion —\nso read the ground truth from the source dataset.) Because the data is freshly generated, your exact\nfailures will differ, but they consistently split into two recognizable kinds — and <strong>neither is\njust \"the model is dumb\". Representative examples:</strong></p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Promptfoo report — per-row pass/fail across the 40-case dataset\" src=\"https://development-wec.wiline.com/docs/assets/images/dataset-report-541b1a9921d846d5338c1b1547c49bd5.png\" width=\"2526\" height=\"1434\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 5.</strong> The web report makes it easy to scan which rows failed and open each one.</p>\n<table><thead><tr><th>Ticket</th><th>Label</th><th>Why it's tricky</th></tr></thead><tbody><tr><td>\"educational discounts for startups…\"</td><td><code>other</code></td><td><strong>Debatable</strong> — it's about pricing, so <code>billing</code> is defensible</td></tr><tr><td>\"grant a junior dev IAM access to S3…\"</td><td><code>account</code></td><td><strong>Debatable</strong> — access control vs infrastructure</td></tr><tr><td>\"feedback on the API documentation…\"</td><td><code>other</code></td><td><strong>Debatable</strong> — feedback vs <code>technical</code></td></tr><tr><td>\"update the card <strong>and</strong> add a user…\"</td><td><code>account</code></td><td><strong>Multi-intent</strong> — genuinely two categories at once</td></tr></tbody></table>\n<p>The rest are ordinary misclassifications. The real, day-to-day lessons:</p>\n<ul>\n<li class=\"\"><strong>Your synthetic labels are not gospel.</strong> A chunk of \"failures\" are label <em>disputes</em> — review and\nfix them, or your eval measures the wrong thing.</li>\n<li class=\"\"><strong>Single-label classification breaks on multi-intent tickets.</strong> Real inputs aren't always one category.</li>\n<li class=\"\"><strong>An 85% headline hides both.</strong> You only learn <em>why</em> by reading the failing rows — which is the\nentire reason to eval on a real dataset instead of two examples.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-we-built\">What we built<a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#what-we-built\" class=\"hash-link\" aria-label=\"Direct link to What we built\" title=\"Direct link to What we built\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">The core skill: a repeatable pipeline to <strong>generate, validate, dedupe, and version</strong> an eval\ndataset from the WEC API — real test data instead of two hand-typed cases.</li>\n<li class=\"\">The discipline that makes it trustworthy: <strong>generated labels aren't gospel.</strong> You validate enums\nand balance, and you <em>review the disputes</em> — a chunk of \"failures\" are your own labels being wrong.</li>\n<li class=\"\">Proof it matters: <strong>coverage surfaces what two cases hide</strong> — genuine misclassifications <em>and</em>\ndebatable, multi-intent tickets you'd never think to type by hand.</li>\n<li class=\"\">A bonus the data unlocks: a <strong>trustworthy model comparison</strong> — here a <strong>3B non-reasoning model\nbeat a 122B reasoning one</strong> for structured output, the opposite of \"bigger is better.\"</li>\n</ul>\n<p>Commit <code>tickets_dataset.jsonl</code> and <code>tests.csv</code> alongside your eval config, and regenerate/grow\nthe dataset as your product changes.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting\">Troubleshooting<a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#troubleshooting\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting\" title=\"Direct link to Troubleshooting\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"401--invalid-api-key\"><a id=\"ts-401\"></a>401 / invalid API key<a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#401--invalid-api-key\" class=\"hash-link\" aria-label=\"Direct link to 401--invalid-api-key\" title=\"Direct link to 401--invalid-api-key\" translate=\"no\">​</a></h3>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">{ \"error\": { \"message\": \"Authentication Error, invalid API key\", \"code\": \"401\" } }</span><br></div></code></pre></div></div>\n<p>The key is expired, revoked, or malformed. Generate a fresh one in <strong>Inference → API Keys</strong>,\nre-export it, and re-sanitize (<code>tr -cd '[:print:]'</code>) in case the paste carried an invisible character.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"jq-cannot-index-array-with-string\"><a id=\"ts-jq\"></a>jq: \"Cannot index array with string\"<a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#jq-cannot-index-array-with-string\" class=\"hash-link\" aria-label=\"Direct link to jq-cannot-index-array-with-string\" title=\"Direct link to jq-cannot-index-array-with-string\" translate=\"no\">​</a></h3>\n<p>This comes from <code>[\"billing\", ...] | index(.category)</code> — inside the pipe, <code>.</code> is the array, so\n<code>.category</code> indexes the array. Use the <code>IN()</code> idiom instead:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'select((.category|IN(\"billing\",\"technical\",\"account\",\"other\")|not))'</span><span class=\"token plain\"> tickets.jsonl</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The jq &amp;quot;Cannot index array with string&amp;quot; error\" src=\"https://development-wec.wiline.com/docs/assets/images/dataset-ts-jq-index-ec85a2581780363d791db73ff2f03fd0.png\" width=\"2016\" height=\"1402\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 6.</strong> The <code>index(.field)</code> mistake — inside the pipe, jq tries to index the array with a string.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"your-dataset-has-rows-that-are-just-null\"><a id=\"ts-null\"></a>Your dataset has rows that are just <code>null</code><a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#your-dataset-has-rows-that-are-just-null\" class=\"hash-link\" aria-label=\"Direct link to your-dataset-has-rows-that-are-just-null\" title=\"Direct link to your-dataset-has-rows-that-are-just-null\" translate=\"no\">​</a></h3>\n<p>A <code>null</code> row means a generation call failed but the loop still wrote its (empty) result. Usual\ncauses: an expired/revoked API key, or the model timing out. Fixes: extract with\n<code>.choices[0].message.content // empty</code> so a failed call writes <strong>nothing</strong> instead of <code>null</code>;\nre-check <code>wc -l</code> after every generation run; and if calls hang, add a per-request timeout\n(<code>curl -m 60</code>) and consider a lighter, faster model for bulk generation.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"__expected-is-null-in-the-results-json\"><code>__expected</code> is <code>null</code> in the results JSON<a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#__expected-is-null-in-the-results-json\" class=\"hash-link\" aria-label=\"Direct link to __expected-is-null-in-the-results-json\" title=\"Direct link to __expected-is-null-in-the-results-json\" translate=\"no\">​</a></h3>\n<p>That's expected — Promptfoo <strong>consumes</strong> the <code>__expected</code> CSV column as the row's assertion, so\nit isn't kept as a variable. To inspect ground-truth labels for failing rows, read them from your\nsource dataset (<code>tickets_dataset.jsonl</code>) rather than the results file.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Real test data at scale</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>You now have a real dataset — but it runs in CI, offline. The next step is watching your app in\n<strong>production</strong>: self-hosting <strong>Langfuse</strong> to trace every call, track latency and cost, and run\nevals (using <em>this dataset</em>) against live traffic. That's where offline testing becomes real\nobservability — and it's the next post in the series.</p>",
            "url": "https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/",
            "title": "Stop hand-writing test cases: generate an eval dataset with the WEC API",
            "summary": "Two hand-typed test cases aren't an eval. Use the WEC Inference API to generate a labeled evaluation dataset — then validate and curate it, because generated labels aren't automatically correct. The result is real test data at scale; feed it to your harness and coverage surfaces the misclassifications and debatable labels two cases would hide. Every command and result is real.",
            "date_modified": "2026-07-06T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "evals",
                "promptfoo",
                "inference",
                "datasets",
                "synthetic-data",
                "observability"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-avatar-31b32c657d72dac0a7edbd77c24ec717.jpg\" alt=\"Promptfoo\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"data:image/png;base64,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\" alt=\"JSON\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 8 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->8</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->8<!-- --> earned</span></div><div class=\"skillTracker__series\">AI evals &amp; observability</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Prove a model works</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">2</span><span class=\"skillTracker__skill\" data-state=\"current\">Trustworthy JSON</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real test data at scale</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Observe &amp; score production</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">RAG, end to end</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">Catch regressions in CI</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trace &amp; debug agent tool calls</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/web-search-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Add live web search</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>\"Return only JSON\" is one of the most common instructions in production LLM apps — and one\nof the least reliable. Models wrap JSON in markdown fences, add a friendly sentence, or (if\nthey're reasoning models) narrate their entire thought process around it. Any of those\nbreaks a strict <code>JSON.parse</code>, and your pipeline falls over.</p>\n<p>In this guide we build a <a href=\"https://www.promptfoo.dev/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Promptfoo</a> eval that makes <strong>WEC\nInference API</strong> models classify support tickets into <strong>schema-validated JSON</strong>, then harden\nit against real-world messiness — and use it to pick a model you can actually trust. This is\npart 2 of the evals series (see <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\">part 1</a>\nfor first-run setup). Everything here was run live against <code>https://inference.wiline.com</code>.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"how-the-eval-works\">How the eval works<a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#how-the-eval-works\" class=\"hash-link\" aria-label=\"Direct link to How the eval works\" title=\"Direct link to How the eval works\" translate=\"no\">​</a></h2>\n<p>Every test runs through the same pipeline. The detail that matters most: the <strong><code>transform</code>\nruns on the model's output <em>before</em> the assertion sees it</strong> — that's what lets us recover\nmessy JSON before validating it.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">\n<p>A <strong>WEC Inference API key</strong> — see <a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/inference/api_keys/\">Inference → API Keys</a>.</p>\n</li>\n<li class=\"\">\n<p>The model names used here (<code>GLM-5.2</code>, <code>gemma4</code>, <code>Qwen2.5-3B-Instruct</code>, <code>qwen3.5:9B</code>) come\nfrom the <a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/inference/models_hub/\">Models Hub</a> — swap in whatever you've enabled.</p>\n</li>\n<li class=\"\">\n<p><strong>Node.js 22+</strong> and your key exported (Promptfoo runs via <code>npx</code>):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">nvm use </span><span class=\"token number\" style=\"color:#36acaa\">24</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">WEC_API_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">'sk-your-key'</span><br></div></code></pre></div></div>\n</li>\n</ul>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>If your first run errors with <code>Cannot convert argument to a ByteString</code>, your pasted key has\nan invisible character — see <a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#ts-invisible-key\" class=\"\">Troubleshooting</a> at the bottom.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--a-strict-schema-and-a-real-failure\">Step 1 — A strict schema, and a real failure<a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#step-1--a-strict-schema-and-a-real-failure\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — A strict schema, and a real failure\" title=\"Direct link to Step 1 — A strict schema, and a real failure\" translate=\"no\">​</a></h2>\n<p>We'll do something closer to a real workload than \"capital of France\": classify a support\nticket into <code>category</code>, <code>priority</code>, and a short <code>summary</code>. The key upgrade over a basic\ncheck is the <strong>JSON Schema</strong> — it enforces <code>enum</code>s, so a model that invents a category fails.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">description: \"WEC Inference API — structured output (JSON schema)\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">providers:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:zai-org/GLM-5.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiBaseUrl: https://inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiKeyEnvar: WEC_API_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      temperature: 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">prompts:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - |</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    Classify this support ticket. Return ONLY JSON with keys:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"category\" (one of: billing, technical, account, other),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"priority\" (one of: low, medium, high),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"summary\" (string, max 12 words).</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    Ticket: {{ticket}}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">defaultTest:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  assert:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    - type: is-json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      value:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        type: object</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        required: [category, priority, summary]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        properties:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          category: { type: string, enum: [billing, technical, account, other] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          priority: { type: string, enum: [low, medium, high] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          summary: { type: string }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">tests:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"I was charged twice for my instance this month.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: billing }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"My VM won't boot after the latest snapshot restore.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: technical }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml</span><br></div></code></pre></div></div>\n<p>The result — <strong>50%</strong>, and the failure is instructive:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">ticket                                    [zai-org/GLM-5.2]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">I was charged twice for my instance…      [FAIL] ```json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                          { \"category\": \"billing\", \"priority\": \"high\", … }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">                                          ```</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">My VM won't boot after the latest…        [PASS] { \"category\": \"technical\", … }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">✓ 1 passed (50%)   ✗ 1 failed (50%)</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Step 1 — one ticket fails because the JSON is wrapped in markdown fences\" src=\"https://development-wec.wiline.com/docs/assets/images/json-step1-schema-fail-dc2b9c12c0ee29b0086499d5c76f2fd6.png\" width=\"964\" height=\"756\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> Same model, same prompt, temperature 0 — one response is clean, the other is\nwrapped in <code>```json</code> fences, so <code>is-json</code> can't parse it.</p>\n<p>The <em>content</em> is correct; the <em>format</em> isn't. <code>is-json</code> validates both \"is it parseable JSON\"\nand \"does it match the schema\" — and fenced output fails the first test.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--recover-fenced-output-with-a-transform\">Step 2 — Recover fenced output with a <code>transform</code><a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#step-2--recover-fenced-output-with-a-transform\" class=\"hash-link\" aria-label=\"Direct link to step-2--recover-fenced-output-with-a-transform\" title=\"Direct link to step-2--recover-fenced-output-with-a-transform\" translate=\"no\">​</a></h2>\n<p>A <code>transform</code> runs on the model's output <strong>before</strong> assertions. Here we strip the fences.\n(This block uses four backticks because the config itself contains triple backticks.)</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">description: \"WEC Inference API — fence-stripping transform\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">providers:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:zai-org/GLM-5.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiBaseUrl: https://inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiKeyEnvar: WEC_API_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      temperature: 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">prompts:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - |</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    Classify this support ticket. Return ONLY JSON with keys:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"category\" (one of: billing, technical, account, other),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"priority\" (one of: low, medium, high),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"summary\" (string, max 12 words).</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    Ticket: {{ticket}}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">defaultTest:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  options:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    transform: |</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      return output.replace(/```json\\n?|\\n?```/g, '').trim();</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  assert:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    - type: is-json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      value:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        type: object</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        required: [category, priority, summary]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        properties:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          category: { type: string, enum: [billing, technical, account, other] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          priority: { type: string, enum: [low, medium, high] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          summary: { type: string }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">tests:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"I was charged twice for my instance this month.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: billing }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"My VM won't boot after the latest snapshot restore.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: technical }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml --no-cache</span><br></div></code></pre></div></div>\n<p>Now both pass — <strong>2/2</strong>. The fences are stripped before <code>is-json</code> sees the output.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Step 2 — with the fence-stripping transform, both tickets pass\" src=\"https://development-wec.wiline.com/docs/assets/images/json-step2-transform-pass-14cb388c6725d0aacffeee1bb320f553.png\" width=\"966\" height=\"735\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> The <code>transform</code> removes the code fences, so the previously-failing ticket parses.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span><code>transform</code> needs an explicit <code>return</code></div><div class=\"admonitionContent_BuS1\"><p>A multi-line <code>transform</code> is a function body, not an expression — omit <code>return</code> and you get\n<code>Transform function did not return a value</code>. See <a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#ts-transform-return\" class=\"\">Troubleshooting</a>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--the-cleaner-fix-json-mode\">Step 3 — The cleaner fix: JSON mode<a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#step-3--the-cleaner-fix-json-mode\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — The cleaner fix: JSON mode\" title=\"Direct link to Step 3 — The cleaner fix: JSON mode\" translate=\"no\">​</a></h2>\n<p>Rather than clean up after the model, ask the API to only emit valid JSON via\n<code>response_format</code>. No <code>transform</code> this time:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">description: \"WEC Inference API — JSON mode (response_format)\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">providers:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:zai-org/GLM-5.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiBaseUrl: https://inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiKeyEnvar: WEC_API_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      temperature: 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      response_format: { type: json_object }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">prompts:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - |</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    Classify this support ticket. Return ONLY JSON with keys:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"category\" (one of: billing, technical, account, other),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"priority\" (one of: low, medium, high),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"summary\" (string, max 12 words).</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    Ticket: {{ticket}}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">defaultTest:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  assert:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    - type: is-json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      value:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        type: object</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        required: [category, priority, summary]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        properties:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          category: { type: string, enum: [billing, technical, account, other] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          priority: { type: string, enum: [low, medium, high] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          summary: { type: string }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">tests:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"I was charged twice for my instance this month.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: billing }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"My VM won't boot after the latest snapshot restore.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: technical }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml --no-cache</span><br></div></code></pre></div></div>\n<p><strong>2/2 PASS</strong> with no transform — looks like JSON mode works.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Step 3 — JSON mode passes on GLM-5.2\" src=\"https://development-wec.wiline.com/docs/assets/images/json-step3-jsonmode-6cabcc01d214aaa3b14b1a0842eeb720.png\" width=\"962\" height=\"718\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> With <code>response_format: json_object</code>, GLM-5.2 returns clean JSON, no transform needed.</p>\n<p>But before trusting <code>response_format</code>, we need to test it across models — because a single\ngreen result can be luck, not enforcement.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--reliability-matrix-across-wec-models\">Step 4 — Reliability matrix across WEC models<a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#step-4--reliability-matrix-across-wec-models\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Reliability matrix across WEC models\" title=\"Direct link to Step 4 — Reliability matrix across WEC models\" translate=\"no\">​</a></h2>\n<p>Run the JSON-mode config against all four chat models. This is the payoff — a\n<strong>WEC-specific reliability comparison</strong> you can't get from a generic leaderboard.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">description: \"WEC Inference API — JSON-mode reliability across models\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">providers:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:zai-org/GLM-5.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0, response_format: { type: json_object } }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:gemma4</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0, response_format: { type: json_object } }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0, response_format: { type: json_object } }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:qwen3.5:9B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0, response_format: { type: json_object } }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">prompts:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - |</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    Classify this support ticket. Return ONLY JSON with keys:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"category\" (one of: billing, technical, account, other),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"priority\" (one of: low, medium, high),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"summary\" (string, max 12 words).</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    Ticket: {{ticket}}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">defaultTest:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  assert:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    - type: is-json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      value:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        type: object</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        required: [category, priority, summary]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        properties:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          category: { type: string, enum: [billing, technical, account, other] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          priority: { type: string, enum: [low, medium, high] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          summary: { type: string }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">tests:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"I was charged twice for my instance this month.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: billing }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"My VM won't boot after the latest snapshot restore.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: technical }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml --no-cache</span><br></div></code></pre></div></div>\n<p><strong>4/8.</strong> GLM-5.2 and Qwen2.5-3B pass cleanly; <strong>gemma4 and qwen3.5:9B dump their reasoning</strong>\n(\"Thinking Process…\") instead of JSON — despite <code>response_format</code> being set.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Step 4 — with JSON mode, two models pass and two emit reasoning\" src=\"https://development-wec.wiline.com/docs/assets/images/json-matrix-responseformat-d227b3a1ca8bffbf85f89ca2da11eb87.png\" width=\"826\" height=\"983\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 4.</strong> <code>response_format</code> set on all four — but the two reasoning-oriented models ignore it.</p>\n<table><thead><tr><th>Model</th><th>Result</th><th>Tokens (2 calls)</th></tr></thead><tbody><tr><td><code>zai-org/GLM-5.2</code></td><td>✅✅</td><td>202</td></tr><tr><td><code>Qwen2.5-3B-Instruct</code></td><td>✅✅</td><td>243</td></tr><tr><td><code>gemma4</code></td><td>❌❌ (reasoning)</td><td>251</td></tr><tr><td><code>qwen3.5:9B</code></td><td>❌❌ (reasoning)</td><td>1,757</td></tr></tbody></table>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can now <strong>measure which models actually honor JSON mode</strong> — instead of trusting the flag.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--verify-is-response_format-doing-anything\">Step 5 — Verify: is <code>response_format</code> doing anything?<a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#step-5--verify-is-response_format-doing-anything\" class=\"hash-link\" aria-label=\"Direct link to step-5--verify-is-response_format-doing-anything\" title=\"Direct link to step-5--verify-is-response_format-doing-anything\" translate=\"no\">​</a></h2>\n<p>Don't assume the reasoning was <em>caused</em> by JSON mode. Re-run the same matrix <strong>without</strong>\n<code>response_format</code> (using the fence-strip transform instead) and compare:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">description: \"WEC Inference API — matrix without response_format\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">providers:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:zai-org/GLM-5.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0 }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:gemma4</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0 }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0 }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:qwen3.5:9B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0 }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">prompts:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - |</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    Classify this support ticket. Return ONLY JSON with keys:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"category\" (one of: billing, technical, account, other),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"priority\" (one of: low, medium, high),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"summary\" (string, max 12 words).</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    Ticket: {{ticket}}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">defaultTest:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  options:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    transform: |</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      return output.replace(/```json\\n?|\\n?```/g, '').trim();</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  assert:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    - type: is-json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      value:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        type: object</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        required: [category, priority, summary]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        properties:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          category: { type: string, enum: [billing, technical, account, other] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          priority: { type: string, enum: [low, medium, high] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          summary: { type: string }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">tests:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"I was charged twice for my instance this month.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: billing }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"My VM won't boot after the latest snapshot restore.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: technical }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml --no-cache</span><br></div></code></pre></div></div>\n<p>Still <strong>4/8</strong> — the exact same models fail. So <code>response_format</code> <strong>changed nothing</strong>: on WEC\ntoday it's effectively a no-op (the backend isn't enforcing JSON mode). The reasoning\nbehavior belongs to the models, not to the flag.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Step 5 — same 4/8 without response_format, proving it was a no-op\" src=\"https://development-wec.wiline.com/docs/assets/images/json-matrix-transform-77179c366ad47c9eb5b00fe2b5d92381.png\" width=\"824\" height=\"983\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 5.</strong> Identical result without JSON mode — the flag wasn't doing anything.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>note</div><div class=\"admonitionContent_BuS1\"><p>Lesson worth its own callout: <strong>don't attribute a result to a change until you've tested the\nchange in isolation.</strong> One extra run turned a wrong conclusion into the right one.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-6--a-more-robust-transform-extract-the-json\">Step 6 — A more robust transform: extract the JSON<a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#step-6--a-more-robust-transform-extract-the-json\" class=\"hash-link\" aria-label=\"Direct link to Step 6 — A more robust transform: extract the JSON\" title=\"Direct link to Step 6 — A more robust transform: extract the JSON\" translate=\"no\">​</a></h2>\n<p>The reasoning models <em>do</em> emit JSON — it's just buried in prose. Instead of stripping fences,\n<strong>extract the first <code>{ … }</code> block</strong> from anywhere in the output:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">description: \"WEC Inference API — JSON extraction transform\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">providers:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:zai-org/GLM-5.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0 }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:gemma4</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0 }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0 }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:qwen3.5:9B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0 }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">prompts:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - |</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    Classify this support ticket. Return ONLY JSON with keys:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"category\" (one of: billing, technical, account, other),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"priority\" (one of: low, medium, high),</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    \"summary\" (string, max 12 words).</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    Ticket: {{ticket}}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">defaultTest:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  options:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    transform: |</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      const m = output.match(/\\{[\\s\\S]*\\}/);</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      return m ? m[0] : output;</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  assert:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    - type: is-json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      value:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        type: object</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        required: [category, priority, summary]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        properties:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          category: { type: string, enum: [billing, technical, account, other] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          priority: { type: string, enum: [low, medium, high] }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          summary: { type: string }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">tests:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"I was charged twice for my instance this month.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: billing }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { ticket: \"My VM won't boot after the latest snapshot restore.\" }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: technical }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml --no-cache</span><br></div></code></pre></div></div>\n<p><strong>7/8</strong> — a big jump. gemma4 and Qwen2.5 fully recover; qwen3.5:9B recovers one of two. The\none remaining failure is telling: that response emitted JSON <strong>and then kept reasoning</strong> (with\nmore braces), so the greedy <code>{ … }</code> match over-captured and produced invalid JSON.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Step 6 — JSON extraction rescues most models (7/8)\" src=\"https://development-wec.wiline.com/docs/assets/images/json-matrix-extraction-3b34ee4a32f6990e447d9590d51d8911.png\" width=\"827\" height=\"901\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 6.</strong> Extracting the JSON block recovers the chatty models — but it's a heuristic, not\na guarantee (note the one over-capture failure).</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-7--see-it-in-the-browser\">Step 7 — See it in the browser<a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#step-7--see-it-in-the-browser\" class=\"hash-link\" aria-label=\"Direct link to Step 7 — See it in the browser\" title=\"Direct link to Step 7 — See it in the browser\" translate=\"no\">​</a></h2>\n<p>The terminal table is fine for a quick read, but the web report is where a matrix like this\ncomes alive:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest view</span><br></div></code></pre></div></div>\n<p>The dashboard at the top gives you the model-reliability picture at a glance — the <strong>Pass\nRate (%)</strong> bars are the ones to watch here: three models tall, <code>qwen3.5:9B</code> sitting at 50%.\nBelow, each provider is a column you can expand to the <strong>full</strong> response (that's how we pulled\nthe over-capture failure in <a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#ts-reasoning-dump\" class=\"\">Figure 10</a> — the terminal truncates it).</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Promptfoo report: Pass Rate bars per model and expandable per-case output\" src=\"https://development-wec.wiline.com/docs/assets/images/json-view-report-3739e5abd8419ffc8ece8271ff559851.png\" width=\"1896\" height=\"1034\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 7.</strong> The report's dashboard — the Pass Rate bars turn the model matrix into a\none-glance ranking, and any cell expands to the full output.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>note</div><div class=\"admonitionContent_BuS1\"><p>The report's other charts (the prompt-vs-prompt scatter) shine when you're <strong>A/B-testing\nprompts</strong>, not models — a good subject for a later post. For a single-prompt, multi-model run\nlike this one, the Pass Rate bars are the chart that matters.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-8--gate-it-in-ci\">Step 8 — Gate it in CI<a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#step-8--gate-it-in-ci\" class=\"hash-link\" aria-label=\"Direct link to Step 8 — Gate it in CI\" title=\"Direct link to Step 8 — Gate it in CI\" translate=\"no\">​</a></h2>\n<p>Promptfoo exits non-zero when tests fail, so a one-liner is a gate (put it in a script or\n<code>Makefile</code>, never your interactive shell):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"✅ passed\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">||</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"❌ eval failed — blocking\"</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exit</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The gate blocking on a failed eval\" src=\"https://development-wec.wiline.com/docs/assets/images/json-ci-gate-71912b506157c96cfd7daa8e5c81f300.png\" width=\"948\" height=\"977\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 8.</strong> A real failure blocks: 7/8 passed, but <code>qwen3.5:9B</code>'s over-capture (from Step 6)\ntrips the gate — the non-zero exit code is the signal CI uses to block a PR.</p>\n<p>On GitLab:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">eval</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> node</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">22</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">script</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> npx </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">y promptfoo@latest eval </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">c promptfooconfig.yaml</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># WEC_API_KEY set as a masked CI/CD variable — never committed.</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p><strong>Schema-validated JSON is now a CI gate</strong> — a model or prompt change that breaks parsing\nnever ships.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-we-learned\">What we learned<a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#what-we-learned\" class=\"hash-link\" aria-label=\"Direct link to What we learned\" title=\"Direct link to What we learned\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>Model choice dominates.</strong> <code>GLM-5.2</code> and <code>Qwen2.5-3B</code> reliably produce schema-valid JSON\nand are ~8× cheaper than the reasoning models. For structured output on WEC, start there.</li>\n<li class=\"\"><strong>Reasoning models</strong> (<code>qwen3.5:9B</code>, and <code>gemma4</code> on the day we tested) narrate their thinking\nand cost far more — poor fits for strict JSON.</li>\n<li class=\"\"><strong>A <code>transform</code> is your safety net</strong> — fence-stripping recovers the common case; JSON\nextraction recovers most chatty output — but extraction is a heuristic that can over-capture.</li>\n<li class=\"\"><strong><code>response_format</code> is a no-op on WEC today</strong> — verified by isolating it. Don't rely on it;\nvalidate instead.</li>\n<li class=\"\"><strong>The eval is the point.</strong> Every one of these surprises was caught automatically, before it\nreached production.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting\">Troubleshooting<a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#troubleshooting\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting\" title=\"Direct link to Troubleshooting\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"transform-function-did-not-return-a-value\"><a id=\"ts-transform-return\"></a>\"Transform function did not return a value\"<a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#transform-function-did-not-return-a-value\" class=\"hash-link\" aria-label=\"Direct link to transform-function-did-not-return-a-value\" title=\"Direct link to transform-function-did-not-return-a-value\" translate=\"no\">​</a></h3>\n<p>A multi-line <code>transform</code> is compiled as a <strong>function body</strong>, so it needs an explicit\n<code>return</code>. <code>output.replace(...)</code> on its own returns nothing.</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">options</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">transform</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">|</span><span class=\"token scalar string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token scalar string\" style=\"color:#e3116c\">    return output.replace(/.../g, '').trim();   # note the `return`</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The transform-return error\" src=\"https://development-wec.wiline.com/docs/assets/images/json-ts-transform-return-764896c26bc05d65d37eac7808ee06fb.png\" width=\"827\" height=\"894\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 9.</strong> Without <code>return</code>, Promptfoo errors instead of transforming.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"a-model-returns-reasoning-around-the-json\"><a id=\"ts-reasoning-dump\"></a>A model returns reasoning around the JSON<a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#a-model-returns-reasoning-around-the-json\" class=\"hash-link\" aria-label=\"Direct link to a-model-returns-reasoning-around-the-json\" title=\"Direct link to a-model-returns-reasoning-around-the-json\" translate=\"no\">​</a></h3>\n<p>If <code>is-json</code> fails with <strong>\"Expected output to be valid JSON\"</strong> and the response contains a\n\"Thinking Process…\" / \"Final Review…\" narration, you're using a reasoning model. Sometimes it\nemits <em>only</em> reasoning; sometimes — as below — it emits valid JSON and then <strong>keeps talking</strong>,\nso even the Step 6 extraction over-captures and the parse fails. Options: switch to\n<code>GLM-5.2</code> / <code>Qwen2.5-3B</code>, or accept that the extraction transform is best-effort, not a guarantee.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The failing case expanded in the report — JSON followed by trailing reasoning\" src=\"https://development-wec.wiline.com/docs/assets/images/json-ts-reasoning-dump-dbfebf9e8d4f5f89e6b2e14b8e0edf50.png\" width=\"561\" height=\"859\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 10.</strong> The failing case expanded in the report: <code>qwen3.5:9B</code> returned valid JSON and\n<em>then kept reasoning</em> (\"7. Final Review…\"), so the extraction over-captured and the assertion\nreports \"Expected output to be valid JSON.\"</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"cannot-convert-argument-to-a-bytestring\"><a id=\"ts-invisible-key\"></a>\"Cannot convert argument to a ByteString\"<a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#cannot-convert-argument-to-a-bytestring\" class=\"hash-link\" aria-label=\"Direct link to cannot-convert-argument-to-a-bytestring\" title=\"Direct link to cannot-convert-argument-to-a-bytestring\" translate=\"no\">​</a></h3>\n<p>Your pasted API key contains an invisible Unicode character. Strip it:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">WEC_API_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable builtin class-name\" style=\"color:#36acaa\">printf</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'%s'</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">\"</span><span class=\"token variable string variable\" style=\"color:#36acaa\">$WEC_API_KEY</span><span class=\"token variable string\" style=\"color:#e3116c\">\"</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable assign-left variable environment constant\" style=\"color:#36acaa\">LC_ALL</span><span class=\"token variable operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">C </span><span class=\"token variable function\" style=\"color:#d73a49\">tr</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-cd</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'[:print:]'</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The ByteString error from an invisible key character\" src=\"https://development-wec.wiline.com/docs/assets/images/json-ts-bytestring-0bb4979ce94f0775f1fe314315b38e01.png\" width=\"953\" height=\"988\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 11.</strong> A stray character in the key trips the HTTP header before any request is sent.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Trustworthy JSON</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>DeepEval regression suites</strong> — turn these checks into a versioned, CI-gated regression run.</li>\n<li class=\"\"><strong>Tracing</strong> — see <em>why</em> a response went wrong, not just that it failed.</li>\n</ul>\n<p>Have a structured-output case that still slips through? That's the next test to add.</p>",
            "url": "https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/",
            "title": "Trustworthy JSON: schema-validate your model's structured output",
            "summary": "LLMs promise JSON and deliver markdown fences and reasoning. Build a Promptfoo eval that classifies support tickets into schema-validated JSON on the WEC Inference API — with transforms to recover messy output, a model-reliability matrix, and a CI gate. Every command and result is real.",
            "date_modified": "2026-07-01T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "evals",
                "promptfoo",
                "inference",
                "json",
                "structured-output",
                "observability"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/",
            "content_html": "\n<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-avatar-31b32c657d72dac0a7edbd77c24ec717.jpg\" alt=\"Promptfoo\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 8 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->8</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->8<!-- --> earned</span></div><div class=\"skillTracker__series\">AI evals &amp; observability</div><ul class=\"skillTracker__steps\"><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">1</span><span class=\"skillTracker__skill\" data-state=\"current\">Prove a model works</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trustworthy JSON</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real test data at scale</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Observe &amp; score production</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/rag-docs-assistant-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">RAG, end to end</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/deepeval-regression-testing-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">6</span><span class=\"skillTracker__skill\" data-state=\"locked\">Catch regressions in CI</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/\"><span class=\"skillTracker__dot\" data-state=\"locked\">7</span><span class=\"skillTracker__skill\" data-state=\"locked\">Trace &amp; debug agent tool calls</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/web-search-wiline-inference/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Add live web search</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>You wouldn't ship code without tests — but most teams ship LLM features on \"looks good\nto me.\" <strong>Evaluations</strong> (evals) fix that: you define test cases and pass/fail criteria, then\nmeasure your model objectively — catching regressions, comparing models, and gating\ndeploys.</p>\n<p>In this guide you'll build a real eval harness with <a href=\"https://www.promptfoo.dev/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Promptfoo</a>\npointed at the <strong>WEC Inference API</strong> — the same OpenAI-compatible endpoint you call from\nyour apps. Everything here was run live against <code>https://inference.wiline.com</code>; the outputs\nare real.</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">\n<p>A <strong>WEC (WiLine Edge Cloud)</strong> account and an <strong>Inference API key</strong> — see\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/inference/api_keys/\">Inference → API Keys</a>.</p>\n</li>\n<li class=\"\">\n<p><strong>Node.js 22+</strong>. Promptfoo treats Node 20 as end-of-life; with <code>nvm</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">nvm </span><span class=\"token function\" style=\"color:#d73a49\">install</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">24</span><span class=\"token plain\">      </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 24 LTS preferred</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">nvm use </span><span class=\"token number\" style=\"color:#36acaa\">24</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">node</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-v</span><span class=\"token plain\">             </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># v24.x</span><br></div></code></pre></div></div>\n</li>\n</ul>\n<p>We'll run Promptfoo with <code>npx</code> — nothing to install globally.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The WiLine portal API Keys page\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-api-key-a1aca6bfffa7335651ab59ec1c07d8f4.png\" width=\"1642\" height=\"866\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> Create a key under Inference → API Keys, then copy it.</p>\n<p>Put your key in the shell:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">WEC_API_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">'sk-your-key-here'</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>If your first eval errors with <code>Cannot convert argument to a ByteString</code>, see <a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#1-bytestring-error-on-first-eval\" class=\"\">Troubleshooting #1</a> below.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--see-which-models-you-have\">Step 1 — See which models you have<a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#step-1--see-which-models-you-have\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — See which models you have\" title=\"Direct link to Step 1 — See which models you have\" translate=\"no\">​</a></h2>\n<p>The API is OpenAI-compatible, so the model list is a plain <code>GET /v1/models</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> https://inference.wiline.com/v1/models </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$WEC_API_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> jq </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-r</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'.data[].id'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">zai-org/GLM-5.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">gemma4</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">qwen3.5:9B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">whisper-large-v3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">whisper-medium</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">whisper-base</span><br></div></code></pre></div></div>\n<p>The <code>whisper-*</code> models are speech-to-text; the rest are chat models. We'll evaluate the\nchat models, starting with <code>zai-org/GLM-5.2</code>.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--your-first-eval\">Step 2 — Your first eval<a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#step-2--your-first-eval\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Your first eval\" title=\"Direct link to Step 2 — Your first eval\" translate=\"no\">​</a></h2>\n<p>Create a config. <code>apiBaseUrl</code> points Promptfoo at WEC; <code>apiKeyEnvar</code> tells it which env\nvar holds your key:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">description: \"WEC Inference API eval\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">providers:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:zai-org/GLM-5.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiBaseUrl: https://inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiKeyEnvar: WEC_API_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      temperature: 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">prompts:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - \"Answer in a single word. What is the capital of {{country}}?\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">tests:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { country: France }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: Paris }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { country: Japan }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: Tokyo }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: first eval, 2 cases both PASS\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-first-eval-42c0a9a515e6a1e5af7a6cf5c6a30118.png\" width=\"954\" height=\"501\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 4.</strong> Your first passing eval against the WEC Inference API.</p>\n<p><code>icontains</code> is a <strong>deterministic</strong> assertion — a case-insensitive substring check. Cheap,\nfast, and perfect for facts with a known answer.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--the-visual-report\">Step 3 — The visual report<a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#step-3--the-visual-report\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — The visual report\" title=\"Direct link to Step 3 — The visual report\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest view</span><br></div></code></pre></div></div>\n<p>This opens a local web UI with every prompt, output, and pass/fail — far easier to scan\nthan the terminal once you have more than a handful of cases.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Promptfoo web report\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-view-report-b93aa9bcadc4c60e008df1bb5effcd3c.png\" width=\"1904\" height=\"1040\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 5.</strong> <code>promptfoo view</code> — the same results in the browser.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--add-a-second-model\">Step 4 — Add a second model<a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#step-4--add-a-second-model\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Add a second model\" title=\"Direct link to Step 4 — Add a second model\" translate=\"no\">​</a></h2>\n<p>Adding a provider is one extra block. Here we add <code>Qwen2.5-3B-Instruct</code> alongside GLM-5.2\nand run the same two test cases against both:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">description: \"WEC Inference API eval — model comparison\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">providers:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:zai-org/GLM-5.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiBaseUrl: https://inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiKeyEnvar: WEC_API_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      temperature: 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiBaseUrl: https://inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiKeyEnvar: WEC_API_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      temperature: 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">prompts:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - \"Answer in a single word. What is the capital of {{country}}?\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">tests:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { country: France }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: Paris }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { country: Japan }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: Tokyo }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: 2-model comparison, 4 cases all PASS\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-two-model-3c6c51ea8839c03dd51d270c78648ec9.png\" width=\"956\" height=\"590\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 6.</strong> Both models pass. GLM-5.2's results are served from cache (0 new requests); only Qwen2.5 makes live calls.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--grade-quality-not-substrings-llm-rubric\">Step 5 — Grade quality, not substrings (LLM rubric)<a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#step-5--grade-quality-not-substrings-llm-rubric\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — Grade quality, not substrings (LLM rubric)\" title=\"Direct link to Step 5 — Grade quality, not substrings (LLM rubric)\" translate=\"no\">​</a></h2>\n<p>Substring checks can't judge an open-ended answer. A <strong>model-graded</strong> <code>llm-rubric</code> assertion\nuses a model to score the output against criteria — and the grader can run on WEC too, so\nyou need no external provider:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">description: \"WEC Inference API eval — model-graded (LLM rubric)\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">providers:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:zai-org/GLM-5.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiBaseUrl: https://inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiKeyEnvar: WEC_API_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      temperature: 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">defaultTest:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  options:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    provider:                       # the grader model</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      id: openai:chat:zai-org/GLM-5.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      config:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        apiBaseUrl: https://inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        apiKeyEnvar: WEC_API_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">prompts:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - \"Explain what a {{topic}} is in two sentences, for a beginner.\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">tests:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { topic: VPC }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - type: llm-rubric</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        value: \"Describes a VPC as a private, isolated virtual network in the cloud, and is understandable to a beginner.\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: LLM rubric eval, 1 PASS, Grading tokens line visible\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-llm-rubric-f969ad98a43427eacf9f83f13f2e6dde.png\" width=\"957\" height=\"556\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 7.</strong> The run shows two token lines — <code>Eval</code> (75) and <code>Grading</code> (350) — because the rubric check is itself a model call.</p>\n<p>Model-graded checks are powerful, but they aren't free.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-6--guardrails-latency\">Step 6 — Guardrails: latency<a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#step-6--guardrails-latency\" class=\"hash-link\" aria-label=\"Direct link to Step 6 — Guardrails: latency\" title=\"Direct link to Step 6 — Guardrails: latency\" translate=\"no\">​</a></h2>\n<p>A correct answer that takes 30 seconds is still a production problem. Add a <strong>latency</strong>\nguardrail to <code>defaultTest</code> (it applies to every case). Always run with <code>--no-cache</code> — cached\nresponses report ~0 ms and would pass the gate for free:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">description: \"WEC Inference API eval — latency guardrail\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">providers:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:zai-org/GLM-5.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiBaseUrl: https://inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiKeyEnvar: WEC_API_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      temperature: 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">defaultTest:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  assert:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    - type: latency</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      threshold: 5000        # fail anything slower than 5 s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">prompts:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - \"Answer in a single word. What is the capital of {{country}}?\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">tests:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { country: France }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: Paris }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { country: Japan }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: Tokyo }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml --no-cache</span><br></div></code></pre></div></div>\n<p>This <strong>failed</strong> — the answers were correct (<code>Paris</code>, <code>Tokyo</code>) but both calls took ~33 s,\ntripping the 5 s gate:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: latency 5000ms threshold, 2 FAIL, Duration: 33s\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-latency-fail-ab40e8dc4386c6acd496942ee3a490aa.png\" width=\"953\" height=\"518\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 8.</strong> The guardrail fires: correct answers, wrong latency. Duration: 33s.</p>\n<p>Bump the threshold to something realistic and re-run:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/threshold: 5000/threshold: 60000/'</span><span class=\"token plain\"> promptfooconfig.yaml</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml --no-cache</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: latency 60000ms threshold, 2 PASS, Duration: 2s\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-latency-pass-f3fa1a1c9cb0a11a87328b2a15ef1f31.png\" width=\"957\" height=\"538\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 9.</strong> With a 60 s threshold the same calls pass easily — Duration: 2s.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Inference latency varies — calibrate before gating</div><div class=\"admonitionContent_BuS1\"><p>Back-to-back calls to the same model ranged from ~2 s to ~33 s on the same day. Run a\nfew <code>--no-cache</code> passes to get a realistic baseline before locking in your threshold.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-7--guardrails-latency--token-cap\">Step 7 — Guardrails: latency + token cap<a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#step-7--guardrails-latency--token-cap\" class=\"hash-link\" aria-label=\"Direct link to Step 7 — Guardrails: latency + token cap\" title=\"Direct link to Step 7 — Guardrails: latency + token cap\" translate=\"no\">​</a></h2>\n<p>The <code>cost</code> assertion doesn't work against WEC (see <a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#2-cost-assertion-errors-immediately\" class=\"\">Troubleshooting #2</a>). Use latency + <code>max_tokens</code> together as your production guardrail instead:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">description: \"WEC Inference API eval — guardrails (latency + token cap)\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">providers:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:zai-org/GLM-5.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiBaseUrl: https://inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiKeyEnvar: WEC_API_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      temperature: 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      max_tokens: 16</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">defaultTest:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  assert:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    - type: latency</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      threshold: 30000</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">prompts:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - \"Answer in a single word. What is the capital of {{country}}?\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">tests:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { country: France }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: Paris }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { country: Japan }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: Tokyo }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml --no-cache</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: latency 30000ms + max_tokens:16, 2 PASS\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-latency-token-cap-968bf5b53dd3edf6d77129f011e4a66f.png\" width=\"955\" height=\"522\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 11.</strong> With a 30 s threshold and a 16-token cap, both cases pass.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-8--validate-structured-output-json-schema\">Step 8 — Validate structured output (JSON schema)<a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#step-8--validate-structured-output-json-schema\" class=\"hash-link\" aria-label=\"Direct link to Step 8 — Validate structured output (JSON schema)\" title=\"Direct link to Step 8 — Validate structured output (JSON schema)\" translate=\"no\">​</a></h2>\n<p>Real apps want JSON, not prose. <code>is-json</code> checks the output is valid JSON <strong>and</strong> matches a\nschema in one assertion:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">description: \"WEC Inference API eval — JSON schema validation\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">providers:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:zai-org/GLM-5.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiBaseUrl: https://inference.wiline.com/v1</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      apiKeyEnvar: WEC_API_KEY</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      temperature: 0</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">prompts:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - 'Return ONLY a JSON object (no markdown, no prose) with keys \"capital\" (string) and \"population_millions\" (number) for the country {{country}}.'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">defaultTest:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  assert:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    - type: is-json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      value:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        type: object</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        required: [capital, population_millions]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        properties:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          capital: { type: string }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          population_millions: { type: number }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">tests:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { country: France }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: Paris }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { country: Japan }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: Tokyo }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: JSON schema eval, France and Japan both PASS with clean JSON output\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-json-schema-82dcc59881029f9bc0134dcf5a5c5ea2.png\" width=\"955\" height=\"516\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 12.</strong> GLM-5.2 returns compact, schema-valid JSON on the first try.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-9--the-payoff-compare-every-wec-model\">Step 9 — The payoff: compare every WEC model<a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#step-9--the-payoff-compare-every-wec-model\" class=\"hash-link\" aria-label=\"Direct link to Step 9 — The payoff: compare every WEC model\" title=\"Direct link to Step 9 — The payoff: compare every WEC model\" translate=\"no\">​</a></h2>\n<p>Here's what you can't get anywhere else — the same strict-JSON test across <strong>all</strong> the chat\nmodels on your account:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">description: \"WEC Inference API eval — model matrix\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">providers:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:zai-org/GLM-5.2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0 }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:gemma4</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0 }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:Qwen2.5-3B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0 }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - id: openai:chat:qwen3.5:9B</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    config: { apiBaseUrl: https://inference.wiline.com/v1, apiKeyEnvar: WEC_API_KEY, temperature: 0 }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">prompts:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - 'Return ONLY a JSON object (no markdown, no prose) with keys \"capital\" (string) and \"population_millions\" (number) for the country {{country}}.'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">defaultTest:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  assert:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    - type: is-json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      value:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        type: object</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        required: [capital, population_millions]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">        properties:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          capital: { type: string }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">          population_millions: { type: number }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">tests:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { country: France }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: Paris }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { country: Japan }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: Tokyo }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  - vars: { country: Brazil }</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    assert: [{ type: icontains, value: Bras }]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: 4-model matrix results table — GLM, gemma4, Qwen2.5 all PASS; qwen3.5:9B all FAIL with reasoning text\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-model-matrix-table-fcebe135ddd276ef881fc83a726c6b96.png\" width=\"1001\" height=\"972\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 13a.</strong> 12 test cases across 4 models — 9 pass, 3 fail. <code>qwen3.5:9B</code> emits its chain-of-thought instead of JSON.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: model matrix token summary — qwen3.5:9B burned 2,650 tokens vs ~200 for the others\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-model-matrix-summary-96e3ce6b439ae8d487aef486120dbe97.png\" width=\"639\" height=\"319\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 13b.</strong> The token breakdown tells the real story: the reasoning model cost 13× more for the same task.</p>\n<table><thead><tr><th>Model</th><th>Strict JSON?</th><th>Notes</th><th>Tokens (3 calls)</th></tr></thead><tbody><tr><td><code>zai-org/GLM-5.2</code></td><td>✅ all pass</td><td>compact JSON</td><td>177</td></tr><tr><td><code>gemma4</code></td><td>✅ all pass</td><td>integer populations</td><td>207</td></tr><tr><td><code>Qwen2.5-3B-Instruct</code></td><td>✅ all pass</td><td>pretty-printed, precise</td><td>262</td></tr><tr><td><code>qwen3.5:9B</code></td><td>❌ all fail</td><td><strong>reasoning model</strong> — emits its \"thinking\" instead of raw JSON</td><td><strong>2,650</strong></td></tr></tbody></table>\n<p>Two lessons fall out immediately:</p>\n<ul>\n<li class=\"\"><strong><code>qwen3.5:9B</code> is a reasoning model.</strong> It \"thinks out loud,\" so it breaks strict JSON\nunless you strip the reasoning or use a response-format constraint. Picking it for\nstructured output would silently fail.</li>\n<li class=\"\"><strong>Cost varies ~13×.</strong> The reasoning model burned 2,650 tokens for the same task the others\ndid in ~200. For a JSON extraction job, the smaller models are both correct <em>and</em> far\ncheaper.</li>\n</ul>\n<p>That's a model-selection decision you can now defend with data — for <em>your</em> workloads, not a\ngeneric leaderboard.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can now <strong>compare every model on your endpoint with objective, repeatable numbers</strong> —\naccuracy, latency, and cost side by side.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-10--make-it-a-gate\">Step 10 — Make it a gate<a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#step-10--make-it-a-gate\" class=\"hash-link\" aria-label=\"Direct link to Step 10 — Make it a gate\" title=\"Direct link to Step 10 — Make it a gate\" translate=\"no\">​</a></h2>\n<p>Promptfoo exits <strong>non-zero (<code>100</code>)</strong> when any test fails. Verify this first:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> /dev/null </span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token file-descriptor important\">&amp;1</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"exit code: </span><span class=\"token string variable\" style=\"color:#36acaa\">$?</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: exit code: 100\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-exit-code-a3471bde65e7ebf24feaf0d964a3b7eb.png\" width=\"652\" height=\"70\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 14.</strong> Exit code 100 confirms Promptfoo signals failure reliably — exactly what CI needs.</p>\n<p>Wire it up as a gate:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">npx </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> promptfoo@latest </span><span class=\"token builtin class-name\">eval</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> promptfooconfig.yaml </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"✅ eval passed\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:#393A34\">||</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"❌ eval failed — blocking\"</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exit</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: CI gate eval table — same 4-model results, 9 pass / 3 fail\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-ci-gate-table-7067b62a9b737ed79e3906b6da54ea2a.png\" width=\"968\" height=\"989\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 15a.</strong> The gate re-runs the matrix eval from cache — same 9/3 result.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: ❌ eval failed — blocking\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-ci-gate-result-8765593b4f037c480e7fcef844ff6eb4.png\" width=\"959\" height=\"647\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 15b.</strong> Promptfoo exits non-zero, the <code>||</code> branch fires, and the gate blocks.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>Never run the <code>exit 1</code> variant in your interactive shell — see <a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#3-exit-1-closes-your-ssh-session\" class=\"\">Troubleshooting #3</a>.</p></div></div>\n<p>In CI, you don't even need the <code>exit 1</code> — the non-zero exit fails the job. On GitLab:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token key atrule\" style=\"color:#00a4db\">eval</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">image</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"> node</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">22</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token key atrule\" style=\"color:#00a4db\">script</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> npx </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">y promptfoo@latest eval </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\">c promptfooconfig.yaml</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># WEC_API_KEY is set as a masked CI/CD variable — never commit it.</span><br></div></code></pre></div></div>\n<p>Now a prompt change that quietly breaks JSON output, or a model swap that doubles latency,\nfails the pipeline before it reaches users.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>Your eval is now a <strong>deploy gate</strong> — regressions fail CI instead of reaching users.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting-real-errors\">Troubleshooting (real errors)<a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#troubleshooting-real-errors\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting (real errors)\" title=\"Direct link to Troubleshooting (real errors)\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"1-bytestring-error-on-first-eval\">1. ByteString error on first eval<a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#1-bytestring-error-on-first-eval\" class=\"hash-link\" aria-label=\"Direct link to 1. ByteString error on first eval\" title=\"Direct link to 1. ByteString error on first eval\" translate=\"no\">​</a></h3>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">API call error: TypeError: Cannot convert argument to a ByteString because</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">the character at index 32 has a value of 8232 which is greater than 255.</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: ByteString error, both cases ERROR\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-invisible-char-error-540ad4b2ab9242c120ae80870319df01.png\" width=\"996\" height=\"627\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> Every API call errors before reaching the server — the key never even leaves the machine.</p>\n<p><strong>Cause:</strong> The pasted key contains an invisible Unicode character — U+2028 (line separator). Some terminals and clipboard managers insert it silently when copying from certain sources.</p>\n<p><strong>Fix:</strong> Strip non-printable characters from the key:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">export</span><span class=\"token plain\"> </span><span class=\"token assign-left variable\" style=\"color:#36acaa\">WEC_API_KEY</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">$(</span><span class=\"token variable builtin class-name\" style=\"color:#36acaa\">printf</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'%s'</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">\"</span><span class=\"token variable string variable\" style=\"color:#36acaa\">$WEC_API_KEY</span><span class=\"token variable string\" style=\"color:#e3116c\">\"</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable operator\" style=\"color:#393A34\">|</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable assign-left variable environment constant\" style=\"color:#36acaa\">LC_ALL</span><span class=\"token variable operator\" style=\"color:#393A34\">=</span><span class=\"token variable\" style=\"color:#36acaa\">C </span><span class=\"token variable function\" style=\"color:#d73a49\">tr</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable parameter variable\" style=\"color:#36acaa\">-cd</span><span class=\"token variable\" style=\"color:#36acaa\"> </span><span class=\"token variable string\" style=\"color:#e3116c\">'[:print:]'</span><span class=\"token variable\" style=\"color:#36acaa\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">printf</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'len=%s\\n'</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$(</span><span class=\"token string variable builtin class-name\" style=\"color:#36acaa\">printf</span><span class=\"token string variable\" style=\"color:#36acaa\"> %s </span><span class=\"token string variable string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable string variable\" style=\"color:#36acaa\">$WEC_API_KEY</span><span class=\"token string variable string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable operator\" style=\"color:#393A34\">|</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable function\" style=\"color:#d73a49\">wc</span><span class=\"token string variable\" style=\"color:#36acaa\"> </span><span class=\"token string variable parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token string variable\" style=\"color:#36acaa\">)</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># len=25</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: key strip command + len=25 confirmation\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-key-strip-1a9c78d8c230c69db3e89eecfdfa24ca.png\" width=\"712\" height=\"70\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> After stripping, the key length confirms no extra characters remain. Re-run the eval — it passes.</p>\n<hr>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"2-cost-assertion-errors-immediately\">2. Cost assertion errors immediately<a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#2-cost-assertion-errors-immediately\" class=\"hash-link\" aria-label=\"Direct link to 2. Cost assertion errors immediately\" title=\"Direct link to 2. Cost assertion errors immediately\" translate=\"no\">​</a></h3>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Error: Cost assertion does not support providers that do not return cost</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal: cost assertion, 2 errors\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-cost-error-1615b4a5c8b0973274d32c6d658a2e12.png\" width=\"953\" height=\"931\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 10.</strong> The WEC Inference API doesn't return a cost field in its responses.</p>\n<p><strong>Cause:</strong> Promptfoo calculates cost by looking up a price-per-token for the model in its built-in price list and multiplying by the tokens used. It has prices for the big hosted models but not for WEC's self-hosted model names, so it has no price to apply and the assertion fails. (Most APIs, including OpenAI, don't return a dollar cost in the response — tools compute it.)</p>\n<p><strong>Fix:</strong> Skip the <code>cost</code> assertion entirely. Track spend from the <code>Total Tokens</code> line Promptfoo prints at the end of every run, and use <code>max_tokens</code> in the provider config to cap per-call token usage.</p>\n<hr>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"3-exit-1-closes-your-ssh-session\">3. <code>exit 1</code> closes your SSH session<a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#3-exit-1-closes-your-ssh-session\" class=\"hash-link\" aria-label=\"Direct link to 3-exit-1-closes-your-ssh-session\" title=\"Direct link to 3-exit-1-closes-your-ssh-session\" translate=\"no\">​</a></h3>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">❌ eval failed — blocking</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">logout</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Connection to 10.80.4.212 closed.</span><br></div></code></pre></div></div>\n<p><strong>Cause:</strong> Running <code>|| { echo \"❌ eval failed — blocking\"; exit 1; }</code> directly in an interactive shell exits the shell process itself — which, over SSH, closes the connection.</p>\n<p><strong>Fix:</strong> Put the gate in a non-interactive context: a <code>Makefile</code> target, an <code>npm</code> script, a git <code>pre-commit</code> hook, or a CI job. In those environments <code>exit 1</code> fails the job without touching your terminal. If you need to test the gate locally, use <code>|| echo \"❌ eval failed\"</code> (without <code>exit 1</code>) to see the output safely.</p>\n<hr>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Prove a model works</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>You've gone from \"looks good to me\" to a real, automatable eval harness on WEC — with\ndeterministic checks, guardrails, schema validation, model-graded rubrics, a model matrix,\nand a CI gate.</p>\n<p>Coming up in this series:</p>\n<ul>\n<li class=\"\"><strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/\">Trustworthy JSON: schema-validate your model's structured output</a></strong>\n— <strong>now available (part 2):</strong> recover fenced and reasoning-wrapped output with transforms,\ncompare models for JSON reliability, and gate it all in CI.</li>\n<li class=\"\"><strong>Model-graded evals done right</strong> — calibrating <code>llm-rubric</code>, factuality scoring, and\nmulti-judge checks so your grader is trustworthy.</li>\n<li class=\"\"><strong>Catching hallucinations</strong> — pairing these techniques with WEC's\n<a href=\"https://wec.wiline.com/resources/rag-hallucination-eval\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">RAG hallucination evaluation</a>.</li>\n<li class=\"\"><strong>Evaluating audio</strong> — scoring Whisper transcription accuracy on the WEC Inference API.</li>\n</ul>",
            "url": "https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/",
            "title": "Evaluate your models with Promptfoo on the WEC Inference API",
            "summary": "Stop eyeballing LLM output. Build a real evaluation harness with Promptfoo pointed at the WEC Inference API — assertions, latency guardrails, JSON-schema checks, model-graded rubrics, an all-WEC model comparison, and a CI gate. Every command and result is real.",
            "date_modified": "2026-06-30T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "evals",
                "promptfoo",
                "inference",
                "testing",
                "observability"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/run-openclaw-on-wec-models/",
            "content_html": "<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 6 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->6</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->6<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting OpenClaw</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Deploy your own AI assistant</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real HTTPS + auth</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Chat from Telegram</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Private mesh access</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">5</span><span class=\"skillTracker__skill\" data-state=\"current\">Run it on WEC models</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Chat from WhatsApp</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/openclaw-wordmark-003352a1a7f02afc3bd877ae6f2dc175.png\" alt=\"OpenClaw\"><span class=\"tutorialHero__plus\">+</span><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"242\" height=\"255\" fill=\"none\" viewBox=\"0 0 242 255\" class=\"tutorialHero__wiline\"><path fill=\"#4E8EF7\" d=\"M56.711 11.293A195 195 0 0 1 41.899 4.79c4.788 1.85 9.71 4.028 14.812 6.504M73.807 29.35c-1.302-1.446-2.666-3.233-4.02-5.38 1.326 1.44 2.642 3.24 4.02 5.38M69.761 23.354c-.832-.585-1.752-1.472-2.558-2.714.9.567 1.685 1.49 2.558 2.714M62.97 15.774a10.7 10.7 0 0 1-3.158-1.841c.993.299 1.986.956 3.159 1.841M59.715 13.346c-.818-.194-1.782-.674-2.829-1.494.84.175 1.761.691 2.83 1.494M39.317 3.782c-.524.192-1.413.302-2.455.14.594-.255 1.342-.238 2.455-.14M74.906 31.339c-.334-.153-.73-.61-1.117-1.398.358.143.706.619 1.117 1.398M66.024 18.878c-.239.1-.557.05-.752-.276.35-.201.532-.069.752.276M71.773 150.625c3.363 7.291 7.185 14.411 9.983 21.914 4.474 11.997 11.167 20.832 24.296 24.757 10.62 3.175 20.369 1.795 30.093-1.816 9.937-3.689 15.165-11.978 18.501-21.541 4.349-12.466 9.385-24.443 19.668-33.446 11.32-9.909 24.639-14.788 39.429-15.63 8.3-.473 16.649-.081 25.444.444.472 15.673.385 30.825.55 45.974.038 3.499-1.049 4.719-4.557 4.543-4.982-.25-9.987-.079-14.98.009-.209.004-.4.991-.6 1.521-23.106 1.695-37.691 15.22-47.961 34.651-3.387 6.408-5.913 13.38-10.013 19.273-4.368 6.277-10.134 11.582-15.853 17.196-6.439 1.421-12.244 3.342-18.202 4.416-12.627 2.274-24.661.457-35.764-6.356-1.028-.631-2.395-.709-3.604-1.043 0 0-.117-.11-.047-.495-.818-.973-1.706-1.562-2.593-2.15 0 0 .09-.044.05-.335-.513-.545-.988-.798-1.463-1.052 0 0-.07-.123.012-.492-.561-.894-1.205-1.421-1.848-1.947 0 0 .134-.068.102-.28-.321-.442-.61-.671-.898-.901 0 0 .086-.007.074-.336-2.05-3.429-4.089-6.529-6.127-9.629 0 0 .094-.017.123-.365-3.013-6.117-6.056-11.886-9.1-17.655 0 0 .096-.01.088-.342-1.978-4.073-3.89-7.848-5.997-11.51-.246-.428-1.572-.235-2.398-.329 0 0-.226-.202-.114-.613-.213-.499-.537-.587-.861-.674 0 0-.032-.101.02-.455-.62-.846-1.291-1.339-1.964-1.832 0 0 .133-.083.119-.372-2.476-4.624-5.645-8.048-10.914-8.777 0 0 .108-.031.01-.326-1.673-1.444-3.105-2.918-4.85-3.688-9.751-4.308-20.013-5.643-30.63-5.23-7.305.285-7.316.051-7.317-7.446q-.002-18.496-.002-36.992v-3.57h36.106s-.08.019.133.283c1.43.415 2.646.567 3.863.719 0 0-.077.022.113.296 1.804.591 3.417.908 5.03 1.225 0 0 .139-.037.152.369 1.592.962 3.17 1.517 4.748 2.073 0 0-.091.027.052.267.735.415 1.328.59 1.92.765 0 0-.076.033.053.246.477.325.825.438 1.173.551 0 0-.149.018-.07.246.418.239.757.251 1.096.263 0 0 .23.058.189.465 1.522 1.319 3.084 2.231 4.647 3.143 0 0-.105.033-.015.316 2.723 2.764 5.356 5.244 7.989 7.724 0 0-.104.062-.094.352 1.01 1.401 2.01 2.512 3.01 3.624\"></path><path fill=\"#4E8EF7\" d=\"M214.77 52.819c-3.343.846-6.723 1.574-10.024 2.562-11.388 3.408-20.567 10.142-27.166 19.865-5.097 7.511-9.621 15.483-13.685 23.608-6.809 13.613-15.725 24.809-31.343 28.198-24.707 5.362-45.22-.126-57.852-24.564C69.466 92.363 63.8 82.375 57.349 73c-3.369-4.896-8.866-8.326-13.679-13.041-1.11-1.409-1.81-2.472-2.794-2.937-10.697-5.056-21.87-7.3-33.686-5.344-3.877.641-5.72-.297-5.615-4.86.294-12.816.26-25.646.019-38.464C1.509 3.874 3.15 2.5 7.429 2.716c9.774.492 19.564.678 29.347.989 0 0-.076.026.082.257 1.046.122 1.935.012 2.825-.098.575.043 1.15.086 2.038.692 5.265 2.72 10.218 4.877 15.17 7.034 0 0-.108.03-.016.317 1.058.765 2.022 1.245 2.987 1.724 0 0-.113.048-.082.354 1.144.876 2.258 1.446 3.372 2.017 0 0 .18.145.097.506.223.404.53.447.837.49 0 0 .214.13.144.504.244.426.558.477.873.528 0 0 .171.124.108.485.256.412.574.464.893.515 0 0 .167.127.105.493.265.437.592.509.919.58 0 0 .078.108.019.5.861 1.28 1.782 2.166 2.702 3.053 0 0-.101.061-.087.368 1.379 2.093 2.743 3.88 4.107 5.666 0 0-.094.011-.092.297.4.742.795 1.2 1.19 1.656 0 0-.121.02-.165.337.685 1.432 1.413 2.549 2.14 3.665 0 0-.11.024-.129.299.326.776.67 1.277 1.014 1.78 0 0-.084-.005-.1.367.933 4.079.311 8.497 4.648 10.67 4.69 12.164 12.107 21.794 25.852 23.497 19.618 2.431 35.321.062 45.299-24.554 3.398-8.383 7.099-16.902 12.232-24.264 8.353-11.979 21.046-17.795 35.832-20.32 1.666.417 2.649.651 3.624.622 9.93-.298 19.865-.513 29.785-1.032 3.818-.2 4.825 1.198 4.746 4.853-.218 9.967.004 19.944-.109 29.915-.052 4.583 1.149 10.606-1.239 13.302-2.044 2.308-8.394.86-12.842.967-3.82.092-7.644.036-11.467.045.065.31.097.633.213.922.034.084.306.072.469.105M37.585 127.354c-11.585.344-23.368.344-35.907.344v3.57q0 18.496.002 36.992c0 7.497.012 7.731 7.318 7.446 10.616-.413 20.878.922 30.63 5.23 1.743.77 3.176 2.244 4.84 3.744-13.655-7.873-28.93-7.767-44.468-7.398v-52.503c12.516.308 25.063-1.222 37.585 2.575\"></path><path fill=\"#4E8EF7\" d=\"M36.619 3.401c-9.626-.007-19.415-.193-29.19-.685C3.151 2.5 1.51 3.874 1.594 8.354c.241 12.818.275 25.648-.018 38.464-.105 4.563 1.737 5.501 5.614 4.86 11.816-1.956 22.99.288 33.686 5.344.984.465 1.685 1.529 2.616 2.66-13.39-7.538-28.284-7.343-43.479-6.78V0C12.314 1.045 24.388 2.071 36.62 3.401M215.232 52.852c-.625-.066-.897-.054-.931-.138-.116-.29-.148-.612-.212-.922 3.822-.01 7.646.047 11.467-.045 4.448-.108 10.797 1.341 12.842-.967 2.387-2.696 1.186-8.719 1.238-13.302.114-9.971-.109-19.948.109-29.915.08-3.655-.928-5.052-4.746-4.853-9.919.519-19.855.734-29.785 1.032-.975.03-1.958-.205-3.228-.58C214.559 1.945 227.423.991 240.73.003v52.882c-8.276 0-16.656 0-25.498-.033M220.071 177.398c-.271-.573-.08-1.561.129-1.565 4.993-.088 9.998-.259 14.98-.009 3.508.176 4.595-1.044 4.557-4.543-.165-15.149-.078-30.301-.088-45.911.458.372 1.32 1.202 1.324 2.036.079 16.424.059 32.848.059 50.036-6.984 0-13.737 0-20.961-.044M88.39 245.741c1.022.084 2.39.162 3.417.793 11.102 6.813 23.137 8.63 35.763 6.356 5.959-1.073 11.764-2.995 17.871-4.34-5.612 2.132-11.343 5.166-17.325 5.79-13.898 1.451-27.803 1.213-39.725-8.599M66.556 210.212c2.975 5.411 6.018 11.18 9.02 17.344-3.011-5.4-5.982-11.193-9.02-17.344M58.341 197.934c.666-.167 1.992-.36 2.238.068 2.107 3.662 4.018 7.437 5.985 11.557-2.69-3.535-5.376-7.449-8.223-11.625M44.637 185.241c5.099.438 8.268 3.862 10.737 8.564-3.518-2.513-7.043-5.393-10.737-8.564M82.29 48.422c-4.252-1.834-3.63-6.252-4.548-10.37 1.508 3.069 2.986 6.55 4.548 10.37M68.757 146.326c-2.533-2.157-5.166-4.637-7.882-7.455 2.539 2.151 5.16 4.641 7.882 7.455M75.532 228.226a122 122 0 0 1 6.047 9.319c-1.993-2.742-3.986-5.854-6.047-9.319M71.695 150.312c-.922-.799-1.921-1.91-2.906-3.366.952.788 1.89 1.921 2.906 3.366M46.663 130.007c-1.353-.103-2.966-.42-4.767-1.051 1.377.07 2.941.454 4.767 1.051M51.647 132.369a13.1 13.1 0 0 1-4.501-1.77c1.48.227 2.902.852 4.5 1.77M60.724 138.321a17.7 17.7 0 0 1-4.395-2.781c1.501.609 2.869 1.551 4.395 2.781M76.889 35.31c-.674-.781-1.402-1.898-2.068-3.39.711.768 1.362 1.912 2.068 3.39M85.668 243.124c.782.31 1.67.899 2.423 1.825-.862-.29-1.59-.919-2.423-1.825M41.596 128.372c-1.033.176-2.25.024-3.68-.432 1.023-.169 2.26-.032 3.68.432M55.367 194.37c.568.222 1.24.715 1.808 1.548-.637-.199-1.17-.738-1.808-1.548M82.397 239.295c.56.251 1.204.778 1.71 1.628-.633-.235-1.13-.794-1.71-1.628M84.291 241.679c.334.034.809.287 1.298.875-.376.004-.766-.326-1.298-.875M53.64 133.426c-.438.094-1.03-.081-1.764-.543.441-.101 1.026.086 1.764.543M77.805 37.409c-.322-.188-.666-.69-.978-1.52.34.182.648.693.978 1.52M81.588 238.108c.218-.039.507.19.823.707-.233.045-.493-.196-.823-.707M55.906 134.832c-.198.157-.537.145-.92-.161.262-.29.537-.195.92.161M57.276 196.552c.254-.079.578.009.756.361-.377.171-.557.018-.756-.361M54.888 134.25c-.215.129-.563.017-1.038-.351.217-.133.56-.012 1.038.351M64.014 16.858a.545.545 0 0 1-.71-.247c.344-.19.51-.067.71.247M65.027 17.882a.574.574 0 0 1-.734-.272c.35-.193.525-.063.734.272M67.045 19.939c-.244.093-.57.021-.777-.329.358-.192.55-.044.777.329\"></path></svg></div>\n<p>Through <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\">Parts 1–4</a> you deployed\nOpenClaw, secured it with HTTPS, added Telegram, and made it private over a NetBird\nmesh — all pointed at <strong>OpenAI</strong>. This part swaps the model out from under it:\npoint the same agent at <strong>WEC Models</strong> — WiLine's own OpenAI-compatible inference —\nand run it on an open-weight model, <strong>Llama 3.1 8B Instruct</strong>. Same box, no rebuild\n— just a base-URL, key, and model change via the OpenClaw CLI.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>Continues on the <strong>same WEC Instance</strong> from Parts 1–4. WEC Models is an\n<strong>OpenAI-compatible</strong> endpoint at <code>https://inference.wiline.com/v1</code>; every command\nbelow was run against a live OpenClaw gateway container\n(<code>openclaw-openclaw-gateway-1</code>).</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"why-point-openclaw-at-wec-models\">Why point OpenClaw at WEC Models<a href=\"https://development-wec.wiline.com/docs/tutorials/run-openclaw-on-wec-models/#why-point-openclaw-at-wec-models\" class=\"hash-link\" aria-label=\"Direct link to Why point OpenClaw at WEC Models\" title=\"Direct link to Why point OpenClaw at WEC Models\" translate=\"no\">​</a></h2>\n<p>If you self-hosted the agent to begin with, you already care about control — not\ndepending on someone else's cloud, not being one pricing-page email away from a\nrebuild. But there's a gap in that story: the agent runs on your box, and every\nsingle message it handles is still shipped off to OpenAI to think. Self-hosting\nthe agent and outsourcing the model is only half the job. This part closes it.</p>\n<ul>\n<li class=\"\"><strong>One model, end to end on WiLine</strong> — your agent <em>and</em> its model run on WEC; no\nthird-party provider on the hot path.</li>\n<li class=\"\"><strong>Open weights</strong> — Llama 3.1 isn't a closed box; you're not locked to a single\nvendor's API to keep the agent running.</li>\n<li class=\"\"><strong>It's a config change, not a migration</strong> — OpenClaw already speaks the OpenAI\nAPI, and WEC Models is OpenAI-compatible, so you keep your agent, prompts, and\nchannels untouched.</li>\n</ul>\n<p>It's also just cheaper. WEC's own catalog prices <code>Llama3.1-8B-Instruct</code> at\n<strong>$0.15 / 1M input tokens · $0.20 / 1M output tokens</strong>. OpenAI doesn't publish a\nstandalone rate for <code>gpt-5.5</code> (the model OpenClaw defaults to in\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\">Part 1</a>) any more, but its closest\ncurrent family member — GPT-5.6 \"Terra,\" pitched by OpenAI as\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/news/gpt-5-6-token-economics/\">\"performance competitive with GPT-5.5\"</a> — lists at\n<strong>$2.50 / 1M in · $15 / 1M out</strong>. That's an approximate comparison, not an\napples-to-apples benchmark — the honest move, as always on this blog, is to\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\">eval it on your own workload</a>\nrather than trust either vendor's number. But the direction is clear enough to be\nworth the ten minutes this takes.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/run-openclaw-on-wec-models/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">OpenClaw running from <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\">Parts 1–4</a> (any\nof the setups works). This guide assumes the gateway container is named\n<code>openclaw-openclaw-gateway-1</code>.</li>\n<li class=\"\">A <strong>WEC API key</strong>.</li>\n<li class=\"\">Shell access to the OpenClaw box.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--get-a-wec-api-key\">Step 1 — Get a WEC API key<a href=\"https://development-wec.wiline.com/docs/tutorials/run-openclaw-on-wec-models/#step-1--get-a-wec-api-key\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Get a WEC API key\" title=\"Direct link to Step 1 — Get a WEC API key\" translate=\"no\">​</a></h2>\n<p>See <a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/inference/api_keys/\">Inference → API Keys</a> if you\ndon't already have one (the same key from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\">Part 1 of the evals series</a>\nworks fine here).</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The WiLine portal API Keys page\" src=\"https://development-wec.wiline.com/docs/assets/images/promptfoo-api-key-a1aca6bfffa7335651ab59ec1c07d8f4.png\" width=\"1642\" height=\"866\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 1.</strong> Create a key under Inference → API Keys, then copy it.</p>\n<p>Keep it out of your shell history — put it in a file instead of exporting it\ndirectly:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'WEC_API_KEY=sk-wec-...'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;&gt;</span><span class=\"token plain\"> env.local</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-v</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'^#'</span><span class=\"token plain\"> env.local </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/^/export /'</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--find-the-model-id\">Step 2 — Find the model ID<a href=\"https://development-wec.wiline.com/docs/tutorials/run-openclaw-on-wec-models/#step-2--find-the-model-id\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Find the model ID\" title=\"Direct link to Step 2 — Find the model ID\" translate=\"no\">​</a></h2>\n<p>Query the OpenAI-compatible <code>/v1/models</code> endpoint to see what's live on your WEC\nInstance:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-H</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Authorization: Bearer </span><span class=\"token string variable\" style=\"color:#36acaa\">$WEC_API_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> https://inference.wiline.com/v1/models</span><br></div></code></pre></div></div>\n<p>The response lists every model available to your key, alongside the id this\ntutorial uses:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"data\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ...</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"id\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token string\" style=\"color:#e3116c\">\"Llama3.1-8B-Instruct\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token property\" style=\"color:#36acaa\">\"object\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token string\" style=\"color:#e3116c\">\"model\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token property\" style=\"color:#36acaa\">\"created\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token number\" style=\"color:#36acaa\">1677610602</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token property\" style=\"color:#36acaa\">\"owned_by\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token string\" style=\"color:#e3116c\">\"openai\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>Model ids on WEC aren't always the display name shown in the portal — always take\nthe exact <code>id</code> from this response, not a guess.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--point-openclaw-at-wec-models\">Step 3 — Point OpenClaw at WEC Models<a href=\"https://development-wec.wiline.com/docs/tutorials/run-openclaw-on-wec-models/#step-3--point-openclaw-at-wec-models\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — Point OpenClaw at WEC Models\" title=\"Direct link to Step 3 — Point OpenClaw at WEC Models\" translate=\"no\">​</a></h2>\n<p>Don't hand-edit <code>openclaw.json</code> — use <code>openclaw config set</code>. It validates the\nwhole file on every write, so a typo fails loudly instead of quietly breaking\nthe gateway. A custom provider needs its base URL, its API style, and its model\nlist declared <strong>together</strong>, in one write — the schema rejects a provider that's\nmissing any of the three, so build the full object up front:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> openclaw-openclaw-gateway-1 openclaw config </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> models.providers.wec </span><span class=\"token string\" style=\"color:#e3116c\">'{</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  \"baseUrl\": \"https://inference.wiline.com/v1\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  \"api\": \"openai-completions\",</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  \"models\": [</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    {\"id\": \"Llama3.1-8B-Instruct\", \"name\": \"Llama 3.1 8B Instruct\"}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  ]</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">}'</span><span class=\"token plain\"> --strict-json</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Updated models.providers.wec. Change will apply without restarting the gateway.</span><br></div></code></pre></div></div>\n<p>That registers WEC as a provider, but it's not authenticated yet. Don't put the\nkey straight into that JSON — OpenClaw's compose setup keeps API keys out of\nenvironment variables entirely and stores them in a dedicated, file-backed\nsecrets directory instead (<code>OPENCLAW_AUTH_PROFILE_SECRET_DIR</code>, mounted into the\ncontainer). Hand the key to OpenClaw's own auth store and let it do the wiring:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token string variable\" style=\"color:#36acaa\">$WEC_API_KEY</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> openclaw-openclaw-gateway-1 openclaw models auth paste-api-key </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--provider</span><span class=\"token plain\"> wec</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Updated config: $OPENCLAW_HOME/.openclaw/openclaw.json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  Backup: $OPENCLAW_HOME/.openclaw/openclaw.json.bak</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Auth profile: wec:manual (wec/api_key)</span><br></div></code></pre></div></div>\n<p>Check that the provider and its auth resolved together, then make the model\nthe default so every agent picks it up without extra config:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> openclaw-openclaw-gateway-1 openclaw models list</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> openclaw-openclaw-gateway-1 openclaw models </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> wec/Llama3.1-8B-Instruct</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Model                                      Input      Ctx         Local Auth  Tags</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">wec/Llama3.1-8B-Instruct                   text       200k        no    yes   configured</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">openai/gpt-5.5                             text       200k        no    yes   configured,alias:GPT</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Updated config: $OPENCLAW_HOME/.openclaw/openclaw.json</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  Backup: $OPENCLAW_HOME/.openclaw/openclaw.json.bak</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Default model: wec/Llama3.1-8B-Instruct</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal showing openclaw models list with wec/Llama3.1-8B-Instruct configured and set as default\" src=\"https://development-wec.wiline.com/docs/assets/images/openclaw-wec-models-list-bb96e6c9a1dfe7d0c98be4a6b5b40ae6.png\" width=\"682\" height=\"449\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 2.</strong> <code>wec/Llama3.1-8B-Instruct</code>, authenticated and set as the default model.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--verify\">Step 4 — Verify<a href=\"https://development-wec.wiline.com/docs/tutorials/run-openclaw-on-wec-models/#step-4--verify\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Verify\" title=\"Direct link to Step 4 — Verify\" translate=\"no\">​</a></h2>\n<p>OpenClaw can run more than one agent per gateway, so every message needs a\ntarget — pass <code>--agent</code>, matching whatever <code>openclaw agents list</code> calls yours\n(<code>main</code>, unless you renamed it):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> openclaw-openclaw-gateway-1 openclaw agents list</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Agents:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">- main (default)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  Workspace: $OPENCLAW_HOME/.openclaw/workspace</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  Model: wec/Llama3.1-8B-Instruct</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  Routing rules: 0</span><br></div></code></pre></div></div>\n<p>Send it a message and see who answers:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> openclaw-openclaw-gateway-1 openclaw agent </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--agent</span><span class=\"token plain\"> main </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--message</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"What is the capital of France?\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Paris is the capital of France.</span><br></div></code></pre></div></div>\n<p>That's WEC, on Llama 3.1, with no OpenAI in the loop.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal showing the agent answering &amp;quot;Paris is the capital of France.&amp;quot; after switching to WEC\" src=\"https://development-wec.wiline.com/docs/assets/images/openclaw-wec-agent-verified-e5125e1d417f258ba73f9d11b78244ff.png\" width=\"675\" height=\"50\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span>\n<strong>Figure 3.</strong> The agent, running on Llama 3.1 8B Instruct via WEC.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5-optional--turn-off-thinking-mode\">Step 5 (optional) — Turn off thinking mode<a href=\"https://development-wec.wiline.com/docs/tutorials/run-openclaw-on-wec-models/#step-5-optional--turn-off-thinking-mode\" class=\"hash-link\" aria-label=\"Direct link to Step 5 (optional) — Turn off thinking mode\" title=\"Direct link to Step 5 (optional) — Turn off thinking mode\" translate=\"no\">​</a></h2>\n<p>Want it snappier? Turn off thinking mode — it's a slash command inside the\nchat, not a shell command:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> openclaw-openclaw-gateway-1 openclaw agent </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--agent</span><span class=\"token plain\"> main </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--message</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"/think off\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Thinking mode has been turned off.</span><br></div></code></pre></div></div>\n<p>Or make it stick for every session:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> openclaw-openclaw-gateway-1 openclaw config </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> agents.defaults.thinkingDefault </span><span class=\"token string\" style=\"color:#e3116c\">\"off\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Updated agents.defaults.thinkingDefault. No gateway restart needed.</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting-real\">Troubleshooting (real)<a href=\"https://development-wec.wiline.com/docs/tutorials/run-openclaw-on-wec-models/#troubleshooting-real\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting (real)\" title=\"Direct link to Troubleshooting (real)\" translate=\"no\">​</a></h2>\n<p><strong>\"custom model providers must declare models\"</strong> — declare <code>baseUrl</code>, <code>api</code>,\nand <code>models</code> together, in one <code>config set</code> call. A custom provider missing any\nof the three gets rejected; you can't add <code>models</code> in a follow-up write.</p>\n<p><strong>\"models.providers.wec.apiKey is unresolved in the active runtime snapshot\"</strong>\n— don't set <code>apiKey</code> on the provider object; let <code>models auth paste-api-key</code>\nown it (Step 3). Already set one? Remove it:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">exec</span><span class=\"token plain\"> openclaw-openclaw-gateway-1 openclaw config </span><span class=\"token builtin class-name\">unset</span><span class=\"token plain\"> models.providers.wec.apiKey</span><br></div></code></pre></div></div>\n<p><strong>\"No target session selected\"</strong> — always pass <code>--agent &lt;id&gt;</code> with\n<code>openclaw agent</code>. Check the id with <code>openclaw agents list</code> first if you're not\nsure (<code>main</code>, unless you renamed it).</p>\n<p><strong><code>-bash: /think: No such file or directory</code></strong> — <code>/think</code> is a slash command\nfor inside the chat, not a shell command. Pass it through <code>--message</code> instead,\nas in Step 5.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/run-openclaw-on-wec-models/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>With the agent running on our own model, the next step is <strong>measuring</strong> it —\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\">evaluating answer quality with Promptfoo + WEC Models</a>.</p>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Run it on WEC models</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>",
            "url": "https://development-wec.wiline.com/docs/tutorials/run-openclaw-on-wec-models/",
            "title": "Run OpenClaw on WEC Models",
            "summary": "Swap the closed provider for WiLine's own inference: point your self-hosted OpenClaw agent at WEC Models — same box, a base-URL/key/model change, no per-token lock-in.",
            "date_modified": "2026-06-25T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "self-hosting",
                "openclaw",
                "wec-models",
                "inference"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/hermesagent-c76bbe81610ae2e70b0950e0f96bec8a.png\" alt=\"Hermes Agent\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/SQLite370-e02c30e84f9e098787bb57701b0b7232.png\" alt=\"SQLite\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 2 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->2</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->2<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting Hermes</div><ul class=\"skillTracker__steps\"><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">1</span><span class=\"skillTracker__skill\" data-state=\"current\">Agent with persistent memory</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/telegram-on-hermes/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Telegram on Hermes</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p><a href=\"https://hermes-agent.nousresearch.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Hermes Agent</a> is Nous Research's open-source\n(MIT) AI agent — \"the agent that grows with you.\" Its standout feature is\n<strong>persistent memory</strong>: it learns your projects and <strong>doesn't forget across\nrestarts</strong>. This guide deploys it on the <strong>same WEC Instance</strong> you already use for\nOpenClaw, points it at a model, and proves the memory survives a full reboot.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>Runs on the <strong>same WEC Instance</strong> from the OpenClaw series — no new VM. The installer\npulls its own Python (via <code>uv</code>), Node.js browser tools, and a Playwright Chromium\nautomatically. State lives in <code>~/.hermes/</code>. The messaging gateway runs as a <strong>systemd\nuser service</strong>, so it coexists with the OpenClaw Docker stack. Captured live on Ubuntu\n22.04 with <code>hermes</code> <strong>v0.17.0</strong>.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"why-persistent-memory-matters\">Why persistent memory matters<a href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/#why-persistent-memory-matters\" class=\"hash-link\" aria-label=\"Direct link to Why persistent memory matters\" title=\"Direct link to Why persistent memory matters\" translate=\"no\">​</a></h2>\n<p>An agent that forgets on every restart is a toy. Hermes keeps a <strong>durable store on\ndisk</strong> under <code>~/.hermes/</code>:</p>\n<ul>\n<li class=\"\"><strong><code>~/.hermes/memories/MEMORY.md</code></strong> — a human-readable file of facts the agent has\nchosen to remember.</li>\n<li class=\"\"><strong><code>~/.hermes/state.db</code></strong> — a <strong>SQLite</strong> database holding every session and message,\nwith <strong>FTS5 full-text search</strong> indexes layered on top so the agent can search its own\nhistory.</li>\n</ul>\n<p>A deploy, crash, or reboot doesn't wipe what it learned. That's the difference between\na chat demo and an agent you can rely on day to day — and it's exactly what we test at\nthe end.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"the-moving-parts\">The moving parts<a href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/#the-moving-parts\" class=\"hash-link\" aria-label=\"Direct link to The moving parts\" title=\"Direct link to The moving parts\" translate=\"no\">​</a></h2>\n<p>Everything runs on one WEC Instance. Hermes talks to a model over the WEC Inference API,\nkeeps its memory on local disk, and runs as a systemd user service — so it restarts itself\nafter a reboot and its memory is still there:</p>\n<!-- -->\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">An existing <strong>WEC Instance</strong> with shell access (the OpenClaw box works).</li>\n<li class=\"\">A model: either a <strong>Nous Portal</strong> login, or your own provider API key (we use an\nOpenAI key here; <strong>WEC Models</strong> with <code>glm-5.2</code> works too).</li>\n<li class=\"\">Nothing to pre-install — the installer handles Python, Node.js, and the browser\nengine.</li>\n</ul>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--install-hermes\">Step 1 — Install Hermes<a href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/#step-1--install-hermes\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Install Hermes\" title=\"Direct link to Step 1 — Install Hermes\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-fsSL</span><span class=\"token plain\"> https://hermes-agent.nousresearch.com/install.sh </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">bash</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Running the install one-liner — the Hermes installer starts resolving and downloading packages\" src=\"https://development-wec.wiline.com/docs/assets/images/hermes-install-start-1f3d29fcdd9d66dd1e5c1bc28b3c9e5d.png\" width=\"660\" height=\"341\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>The installer resolves a Python virtual environment with <code>uv</code>, installs the Node.js\nbrowser tools, downloads a Playwright Chromium, syncs the bundled skills, and drops a\n<code>hermes</code> launcher into <code>~/.local/bin</code>:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">✓ Main package installed (hash-verified via uv.lock)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">✓ All dependencies installed</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">✓ Browser engine setup complete</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">✓ Installed hermes launcher → ~/.local/bin/hermes</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">✓ Configuration directory ready: ~/.hermes/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">→ Starting setup wizard...</span><br></div></code></pre></div></div>\n<p>It then launches the setup wizard automatically. Reload your shell afterward so the\n<code>hermes</code> command is on your <code>PATH</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">source</span><span class=\"token plain\"> ~/.bashrc</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--run-the-setup-wizard\">Step 2 — Run the setup wizard<a href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/#step-2--run-the-setup-wizard\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Run the setup wizard\" title=\"Direct link to Step 2 — Run the setup wizard\" translate=\"no\">​</a></h2>\n<p>The wizard walks through setup type, terminal backend, and messaging. On a box that\nalready ran OpenClaw, it offers to <strong>import</strong> the old config first — you can preview\nwhat would be migrated before anything changes.</p>\n<p>The first choice is <strong>how to set up Hermes</strong>:</p>\n<ul>\n<li class=\"\"><strong>Quick Setup (Nous Portal)</strong> — one OAuth login, no API keys. The Portal bundles a\nmodel and a tool gateway. <strong>Requires an active Nous Portal subscription.</strong></li>\n<li class=\"\"><strong>Full setup (bring your own keys)</strong> — you configure providers and paste your own\nkeys.</li>\n</ul>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The &amp;quot;How would you like to set up Hermes?&amp;quot; prompt with Quick Setup, Full setup, and Blank Slate options\" src=\"https://development-wec.wiline.com/docs/assets/images/hermes-setup-choice-0db3a3f64faec3d72e3477b075af1901.png\" width=\"658\" height=\"125\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>What we actually did</div><div class=\"admonitionContent_BuS1\"><p>We chose <strong>Quick Setup</strong> first. The Portal login showed a device code and sat at\n<em>\"Waiting for approval (polling)\"</em> — it needs an active Portal subscription, which this\naccount didn't have. We pressed <strong>Ctrl+C</strong> to cancel and instead <strong>brought our own\nmodel in Step 3</strong> (below) with an OpenAI key. If you <em>do</em> have a Portal plan, complete\nthe login here and you can skip Step 3.</p></div></div>\n<p>For the rest of the wizard:</p>\n<ul>\n<li class=\"\"><strong>Terminal backend:</strong> <em>Local</em> — commands run directly on this machine.</li>\n<li class=\"\"><strong>Messaging:</strong> <em>Skip</em> for now (you can wire up Telegram/Discord later with\n<code>hermes setup gateway</code>).</li>\n</ul>\n<p>The wizard applies sensible agent defaults (max iterations, a 50% context-compression\nthreshold) and finishes with a tool-availability summary.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--connect-a-model\">Step 3 — Connect a model<a href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/#step-3--connect-a-model\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — Connect a model\" title=\"Direct link to Step 3 — Connect a model\" translate=\"no\">​</a></h2>\n<p>Out of the box the default model routes through the Nous Portal / OpenRouter and needs\na key. To use <strong>your own provider</strong> instead, run:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">hermes setup model</span><br></div></code></pre></div></div>\n<p>Then select <strong>OpenAI ▸ OpenAI API</strong>, paste your API key when prompted (it's written to\n<code>~/.hermes/.env</code> — never echoed), accept the default base URL\n<code>https://api.openai.com/v1</code>, and pick a model — we used <strong><code>gpt-5.5</code></strong>:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">API key saved.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Base URL [https://api.openai.com/v1]:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Select default model:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"> → (●) gpt-5.5</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Choosing the default model in hermes setup model — gpt-5.5 selected from the OpenAI list\" src=\"https://development-wec.wiline.com/docs/assets/images/hermes-model-select-2b0c5452d2e217ff8692c022badd9b28.png\" width=\"661\" height=\"545\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Use WEC Models instead</div><div class=\"admonitionContent_BuS1\"><p>To run inference on WiLine's own endpoint, choose <strong>Custom endpoint</strong>, set the base URL\nto <code>https://inference.wiline.com/v1</code>, paste your WEC API key, and use a model like\n<code>glm-5.2</code>. Same OpenAI-compatible flow.</p></div></div>\n<p>Confirm the model and key are healthy:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">hermes doctor</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">◆ Configuration Files</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ✓ ~/.hermes/.env file exists</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ✓ API key or custom endpoint configured</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">◆ Memory Provider</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ✓ Built-in memory active (no external provider configured — this is fine)</span><br></div></code></pre></div></div>\n<p>The remaining <code>⚠</code> lines in <code>doctor</code> are optional tool integrations (web search,\nDiscord, image gen) — not needed for this guide.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--chat-and-save-a-memory\">Step 4 — Chat and save a memory<a href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/#step-4--chat-and-save-a-memory\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Chat and save a memory\" title=\"Direct link to Step 4 — Chat and save a memory\" translate=\"no\">​</a></h2>\n<p>Start an interactive session:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">hermes</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The Hermes Agent TUI welcome screen — ASCII logo, available tools and skills, model gpt-5.5, and the chat prompt\" src=\"https://development-wec.wiline.com/docs/assets/images/hermes-tui-welcome-842b22f5c733599e17b84cde75db4933.png\" width=\"657\" height=\"814\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Send it a fact and ask it to remember — watch for the <code>🧠 memory</code> tool to fire:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">In the Hermes TUI</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">● Please save to your long-term memory: my WEC project codename is</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  Bluefin-7, running on the OpenClaw box.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  ┊ 🧠 memory    ?  0.0s</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">╭─ ⚕ Hermes ─────────────────────────────────────────────────────────╮</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    Saved to long-term memory: your WEC project codename is Bluefin-7,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    running on the OpenClaw box.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">╰─────────────────────────────────────────────────────────────────────╯</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Hermes firing the memory tool and confirming the fact was saved to long-term memory\" src=\"https://development-wec.wiline.com/docs/assets/images/hermes-memory-save-458cefa8aaf46e0d1cc554418a3acc38.png\" width=\"663\" height=\"218\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>That write creates <code>~/.hermes/memories/MEMORY.md</code>. Exit with <code>/exit</code> or <code>Ctrl+D</code>.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Paste gotcha</div><div class=\"admonitionContent_BuS1\"><p>The TUI submits on newline, so pasting multi-line text can send a half-typed message.\nIf a prompt only catches a fragment, <strong>type it on one line</strong> by hand.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--prove-memory-survives-a-reboot\">Step 5 — Prove memory survives a reboot<a href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/#step-5--prove-memory-survives-a-reboot\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — Prove memory survives a reboot\" title=\"Direct link to Step 5 — Prove memory survives a reboot\" translate=\"no\">​</a></h2>\n<p>This is the whole point. The Hermes gateway is installed as a <strong>systemd user service</strong>\nwith <strong>linger enabled</strong>, so it comes back automatically after a reboot — no login\nrequired. Restart the service, then reboot the whole box to make the test airtight:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">systemctl </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--user</span><span class=\"token plain\"> restart hermes-gateway   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># restart just the gateway service</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">reboot</span><span class=\"token plain\">                               </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># then reboot the entire box</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Restarting the gateway and rebooting the box — the SSH connection closes as the host goes down\" src=\"https://development-wec.wiline.com/docs/assets/images/hermes-reboot-ae228254579427ae3018547803f83c62.png\" width=\"616\" height=\"74\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Reconnect once the box is up and confirm the service restarted on its own:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">systemctl </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--user</span><span class=\"token plain\"> status hermes-gateway --no-pager</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">● hermes-gateway.service - Hermes Agent Gateway - Messaging Platform Integration</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">     Loaded: loaded (...; enabled; vendor preset: enabled)</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">     Active: active (running) since Thu 2026-06-25 21:21:54 UTC; 1min 36s ago</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"systemctl status showing hermes-gateway active and running since boot, started automatically by linger after the reboot\" src=\"https://development-wec.wiline.com/docs/assets/images/hermes-gateway-status-4da5472c10ec41fe81cfc64298589f10.png\" width=\"1001\" height=\"272\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Now open a <strong>fresh</strong> session and ask — no prior context, brand-new session ID:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">hermes</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">In the Hermes TUI</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">● What's my WEC project codename, and which box does it run on?</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">╭─ ⚕ Hermes ─────────────────────────────────────────────────────────╮</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    Your WEC project codename is Bluefin-7, and it runs on the</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    OpenClaw box.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">╰─────────────────────────────────────────────────────────────────────╯</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"A fresh Hermes session after reboot correctly recalling the Bluefin-7 codename from disk\" src=\"https://development-wec.wiline.com/docs/assets/images/hermes-reboot-recall-eac2b093a48d12569fa730340f5751d3.png\" width=\"652\" height=\"300\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>It recalled the fact from disk after a full reboot. Inspect where that durable state\nlives:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> ~/.hermes/memories/MEMORY.md</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">file</span><span class=\"token plain\"> ~/.hermes/state.db </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">du</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-h</span><span class=\"token plain\"> ~/.hermes/state.db</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">User's WEC project codename is Bluefin-7, running on the OpenClaw box.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">/home/ubuntu/.hermes/state.db: SQLite 3.x database, ...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">176K\t/home/ubuntu/.hermes/state.db</span><br></div></code></pre></div></div>\n<p>A plain-text <code>MEMORY.md</code> you can read and edit, backed by a SQLite <code>state.db</code> for\nsessions. That's the persistent memory — and it just outlived a reboot.</p>\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>How the memory is stored — two files, two jobs</div><div class=\"admonitionContent_BuS1\"><p>When you ask Hermes to remember something, it writes the fact to\n<strong><code>~/.hermes/memories/MEMORY.md</code></strong> — plain Markdown you can open, read, and even edit by\nhand. Separately, your <strong>full conversation history</strong> is kept in\n<strong><code>~/.hermes/state.db</code></strong>, a <strong>SQLite</strong> database. Both live on disk, which is why a\nreboot doesn't lose them.</p><p>The simplest way to see what the agent knows is just to read the Markdown:</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> ~/.hermes/memories/MEMORY.md</span><br></div></code></pre></div></div><p>To look inside the database, note that SQLite's <em>engine</em> is built into Python, so Hermes\nuses it without any extra install. The optional <code>sqlite3</code> <em>command-line tool</em> — for\nbrowsing the DB by hand — isn't installed by default. If you want it:</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">apt</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">install</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-y</span><span class=\"token plain\"> sqlite3</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sqlite3 ~/.hermes/state.db </span><span class=\"token string\" style=\"color:#e3116c\">'.tables'</span><br></div></code></pre></div></div><p><span class=\"zoomImage__wrap\"><img alt=\"Output of .tables on state.db — sessions and messages tables alongside the messages_fts FTS5 family\" src=\"https://development-wec.wiline.com/docs/assets/images/hermes-sqlite-tables-dbdc5016a3514137534038cac3f6298d.png\" width=\"540\" height=\"170\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p><p>You'll see <code>sessions</code> and <code>messages</code> tables holding the conversation, plus a family of\n<code>messages_fts*</code> tables. Those are <strong>SQLite FTS5</strong> full-text indexes — including a\ntrigram variant for fuzzy matching — that let Hermes search its own past conversations:</p><div class=\"language-sql codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output (from .schema)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-sql codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token keyword\" style=\"color:#00009f\">CREATE</span><span class=\"token plain\"> VIRTUAL </span><span class=\"token keyword\" style=\"color:#00009f\">TABLE</span><span class=\"token plain\"> messages_fts </span><span class=\"token keyword\" style=\"color:#00009f\">USING</span><span class=\"token plain\"> fts5</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"color:#00009f\">CREATE</span><span class=\"token plain\"> VIRTUAL </span><span class=\"token keyword\" style=\"color:#00009f\">TABLE</span><span class=\"token plain\"> messages_fts_trigram </span><span class=\"token keyword\" style=\"color:#00009f\">USING</span><span class=\"token plain\"> fts5</span><span class=\"token punctuation\" style=\"color:#393A34\">(</span><span class=\"token plain\">content</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> tokenize</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token string\" style=\"color:#e3116c\">'trigram'</span><span class=\"token punctuation\" style=\"color:#393A34\">)</span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><br></div></code></pre></div></div><p>And each session is a row you can inspect — here are the two from this guide, the\nsecond one created <em>after</em> the reboot:</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sqlite3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-header</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-column</span><span class=\"token plain\"> ~/.hermes/state.db </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token string\" style=\"color:#e3116c\">\"SELECT substr(id,1,15) AS id, model, message_count FROM sessions ORDER BY started_at;\"</span><br></div></code></pre></div></div><div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">id               model    message_count</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">---------------  -------  -------------</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">20260625_211227  gpt-5.5  8</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">20260625_212333  gpt-5.5  2</span><br></div></code></pre></div></div><p>You never <em>need</em> the CLI for Hermes to work — it's only for inspecting the database\nyourself. Day to day, the human-readable <code>MEMORY.md</code> is all you have to look at.</p></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You now run an <strong>agent whose memory survives reboots</strong> — and you know exactly where that\nmemory lives on disk.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"bonus--read-the-conversation-the-agent-stored\">Bonus — read the conversation the agent stored<a href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/#bonus--read-the-conversation-the-agent-stored\" class=\"hash-link\" aria-label=\"Direct link to Bonus — read the conversation the agent stored\" title=\"Direct link to Bonus — read the conversation the agent stored\" translate=\"no\">​</a></h2>\n<p>The <code>messages</code> table holds the actual text of every turn. Each row has a <code>role</code> —\n<code>user</code> (what you typed), <code>assistant</code> (Hermes's reply), or <code>tool</code> (a tool's result) — so\nyou can replay the whole exchange:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sqlite3 </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-header</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-column</span><span class=\"token plain\"> ~/.hermes/state.db </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token string\" style=\"color:#e3116c\">\"SELECT substr(session_id,10,6) AS sess, role, substr(content,1,55) AS content</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">   FROM messages ORDER BY timestamp;\"</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sess    role       content</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">------  ---------  -------------------------------------------------------</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">211227  user       hello, can you hear me?</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">211227  assistant  Yes — I can hear you. How can I help?</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">211227  user       Please save to your long-term memory: my WEC project...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">211227  tool       {\"success\": true, \"done\": true, \"target\": \"memory\", ...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">211227  assistant  Saved to long-term memory: your WEC project codename...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">212333  user       What's my WEC project codename, and which box does...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">212333  assistant  Your WEC project codename is Bluefin-7, and it runs...</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The messages table in state.db showing the full conversation — user, assistant, and tool rows across both sessions\" src=\"https://development-wec.wiline.com/docs/assets/images/hermes-sqlite-messages-12ab80eeb7db27fbf02d5c73b075d553.png\" width=\"659\" height=\"294\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>The interesting part is <em>how</em> the memory got written. The assistant didn't edit a file\ndirectly — it emitted a <strong><code>memory</code> tool call</strong>, and that tool is what wrote\n<code>MEMORY.md</code>. You can see the exact call recorded in the history:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">sqlite3 ~/.hermes/state.db </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token string\" style=\"color:#e3116c\">\"SELECT tool_calls FROM messages WHERE tool_calls LIKE '%memory%' LIMIT 1;\"</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"function\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token property\" style=\"color:#36acaa\">\"name\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"memory\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> </span><span class=\"token property\" style=\"color:#36acaa\">\"arguments\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  \"</span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">\\\"target\\\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\">\\\"memory\\\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\">\\\"operations\\\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token punctuation\" style=\"color:#393A34\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">     </span><span class=\"token punctuation\" style=\"color:#393A34\">{</span><span class=\"token plain\">\\\"action\\\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\">\\\"add\\\"</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      \\\"content\\\"</span><span class=\"token operator\" style=\"color:#393A34\">:</span><span class=\"token plain\">\\\"User's WEC project codename is Bluefin</span><span class=\"token number\" style=\"color:#36acaa\">-7</span><span class=\"token punctuation\" style=\"color:#393A34\">,</span><span class=\"token plain\"> running on the OpenClaw box.\\\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">]</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token plain\">\"</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><span class=\"token punctuation\" style=\"color:#393A34\">}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The recorded memory tool call in the messages table — the add operation that wrote the Bluefin-7 fact to MEMORY.md\" src=\"https://development-wec.wiline.com/docs/assets/images/hermes-sqlite-memory-toolcall-53bc3762b14f9ca8e30715118341b792.png\" width=\"897\" height=\"362\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>So the full chain is: <strong>you type → the agent decides it's worth keeping → it calls the\n<code>memory</code> tool with <code>action: add</code> → that writes the fact into <code>MEMORY.md</code></strong>, while the\nwhole conversation stays in <code>state.db</code>, full-text searchable via FTS5. Two stores, one\ndurable memory.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-can-you-do-with-it\">What can you do with it?<a href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/#what-can-you-do-with-it\" class=\"hash-link\" aria-label=\"Direct link to What can you do with it?\" title=\"Direct link to What can you do with it?\" translate=\"no\">​</a></h2>\n<p>A memory that survives reboots only matters because of what the agent <em>does</em> with it.\nWith Hermes running, the same install ships these capabilities (you saw them in the\n<code>hermes doctor</code> tool list) — here's what they unlock day to day:</p>\n<ul>\n<li class=\"\"><strong>Run real work on the box.</strong> The <code>terminal</code> and <code>code_execution</code> tools let it act,\nnot just chat — e.g. <em>\"clone this repo, install deps, and run the tests,\"</em> or <em>\"check\nwhy the disk is filling up.\"</em> It does it on the WEC Instance and reports back.</li>\n<li class=\"\"><strong>Drive it from your phone.</strong> A messaging gateway (Telegram, Discord, Slack, and\nmore) means you can hand it a task from anywhere — no SSH. <em>(Setup in Part 2.)</em></li>\n<li class=\"\"><strong>Put it on a schedule.</strong> The <code>cronjob</code> tool runs recurring jobs: a <strong>morning\nbriefing</strong>, a <strong>weekly disk-usage report</strong>, or <strong>watch a service and alert you when\nit fails</strong> — delivered to your messaging channel. <em>(Part 3.)</em></li>\n<li class=\"\"><strong>Browse and search the web.</strong> With the browser and web-search tools it can pull live\ninformation and act on real pages. <em>(These need provider API keys — see <code>hermes setup tools</code>.)</em></li>\n<li class=\"\"><strong>Grow reusable skills and delegate.</strong> It can build <strong>skills</strong> from what it learns and\nhand subtasks to <strong>subagents</strong> — the \"agent that grows with you\" part.</li>\n<li class=\"\"><strong>Remember across all of it.</strong> Everything above is backed by the persistent memory you\njust set up, so context carries from one task — and one day — to the next.</li>\n</ul>\n<p>The rest of this series turns each of these into a hands-on guide (see <em>What's next</em>).</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting-real\">Troubleshooting (real)<a href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/#troubleshooting-real\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting (real)\" title=\"Direct link to Troubleshooting (real)\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">\n<p><strong>Quick Setup (Nous Portal) hangs at \"Waiting for approval (polling)\".</strong> The Portal\nlogin needs an active subscription/credit. If you don't have one, <code>Ctrl+C</code> and use\n<em>Full setup</em> with your own key instead (Step 3).</p>\n</li>\n<li class=\"\">\n<p><strong><code>doctor</code> shows \"No API key found in ~/.hermes/.env\".</strong> The default model points at\nthe Portal/OpenRouter. Run <code>hermes setup model</code>, pick your provider, and paste the\nkey — <code>doctor</code> then reports <em>\"API key or custom endpoint configured.\"</em></p>\n</li>\n<li class=\"\">\n<p><strong>Deprecated <code>.env</code> warning on gateway start</strong> (e.g. <code>MESSAGING_CWD</code>). Harmless;\nfollow the hint to move the setting into <code>config.yaml</code> when convenient.</p>\n</li>\n<li class=\"\">\n<p><strong>Repeating <code>WARNING [Telegram] …</code> in the gateway logs.</strong> If you followed the earlier\nOpenClaw parts and added a Telegram channel, Hermes's setup detected OpenClaw and\n<strong>imported those messaging settings</strong> into <code>~/.hermes/.env</code>. Because we skipped\nfinishing messaging here, the gateway has a token but no complete config, so it keeps\nretrying and logging this warning. (A fresh box with no OpenClaw Telegram setup won't\nsee it.) Remove the stray entries and restart:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'/^TELEGRAM_ALLOWED_USERS=/d; /^TELEGRAM_BOT_TOKEN=/d'</span><span class=\"token plain\"> ~/.hermes/.env</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">systemctl </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--user</span><span class=\"token plain\"> restart hermes-gateway</span><br></div></code></pre></div></div>\n<p>If that token was ever exposed, revoke it in Telegram's <strong>@BotFather</strong> and issue a new\none.</p>\n</li>\n</ul>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Agent with persistent memory</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>Now that Hermes is running with durable memory, the rest of the series turns each\ncapability above into a hands-on guide:</p>\n<ul>\n<li class=\"\"><strong>Part 2 — Drive Hermes from Telegram.</strong> Finish migrating the Telegram channel Hermes\nalready imported from OpenClaw (<code>hermes setup gateway</code>, allowlists, verify the bot\nresponds), so you can hand it tasks from your phone.</li>\n<li class=\"\"><strong>Part 3 — Put it on a schedule.</strong> Use the cron tool for a morning briefing, a weekly\nreport, and a service-health alert delivered to your channel.</li>\n<li class=\"\"><strong>Part 4 — Custom skills &amp; subagents.</strong> Teach it reusable skills and delegate work.</li>\n</ul>\n<p>Links will appear here as each part ships.</p>",
            "url": "https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/",
            "title": "Self-host the Hermes Agent with persistent memory",
            "summary": "Deploy Nous Research's open-source Hermes Agent on your WEC Instance — with SQLite-backed persistent memory that survives a full reboot. Real install, model config, and a memory-survives-restart test.",
            "date_modified": "2026-06-25T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "self-hosting",
                "hermes",
                "agents",
                "memory"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/openclaw-wordmark-003352a1a7f02afc3bd877ae6f2dc175.png\" alt=\"OpenClaw\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__docker\" src=\"https://development-wec.wiline.com/docs/assets/images/netbird-dark-0d2ba9c783792904def84b7e687b8f95.png\" alt=\"NetBird\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 6 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->6</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->6<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting OpenClaw</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Deploy your own AI assistant</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real HTTPS + auth</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Chat from Telegram</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">4</span><span class=\"skillTracker__skill\" data-state=\"current\">Private mesh access</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/run-openclaw-on-wec-models/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run it on WEC models</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Chat from WhatsApp</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>In <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/\">Article 2</a> we put Caddy in front of\nOpenClaw for HTTPS, and in <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/\">Article 3</a>\nwe added a Telegram channel. The gateway works — but Caddy is still listening on\n<code>0.0.0.0</code>, reachable by anything that can route to the box. This is the capstone\nof the series: we join the server and your laptop to a <strong>NetBird</strong> mesh,\nrepoint <code>openclaw.local</code> at the mesh IP, and <strong>close the public ports</strong>. The same\n<code>https://openclaw.local/chat</code> URL keeps working — but only for your devices.\nEvery command and error below is from the actual run.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>Continues from Articles 1–3 on the same WEC Instance — <strong>Ubuntu</strong>, OpenClaw behind\n<strong>Caddy 2</strong>. We use <strong>managed NetBird</strong> (the free tier at\n<a href=\"https://app.netbird.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">app.netbird.io</a>) with agent <strong>0.73.2</strong> on both the\nserver (Linux, kernel WireGuard) and the client (macOS, userspace). NetBird is\n<a href=\"https://www.wireguard.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">WireGuard</a>-based and open source; you can also\nself-host the control plane, which is a bigger topic for another day.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"how-netbird-works-and-what-youll-build\">How NetBird works (and what you'll build)<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#how-netbird-works-and-what-youll-build\" class=\"hash-link\" aria-label=\"Direct link to How NetBird works (and what you'll build)\" title=\"Direct link to How NetBird works (and what you'll build)\" translate=\"no\">​</a></h2>\n<p>NetBird has two parts, and the distinction is the whole point:</p>\n<ol>\n<li class=\"\"><strong>The control plane</strong> (NetBird's managed <em>management</em> + <em>signal</em> service) — this is the only thing with a public presence. Every device that wants to join <strong>authenticates to it</strong> (with your setup key or SSO login), and it brokers the exchange of each peer's <a href=\"https://www.wireguard.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">WireGuard</a> public key.</li>\n<li class=\"\"><strong>The data plane</strong> — once authenticated, peers talk <strong>directly to each other</strong>, end-to-end encrypted over WireGuard. Your actual traffic never flows through a public port you manage; the control plane just did the introductions.</li>\n</ol>\n<p>So you're building a mesh where <strong>Peer A</strong> (your laptop) reaches <strong>Peer B</strong> (the OpenClaw box) by its private <code>100.x</code> address — and <code>openclaw.local</code> points there, with Caddy bound to that interface only. The public/LAN side has nothing listening.</p>\n<p>The key idea for later: <strong>the mesh isn't limited to two peers.</strong> Authenticate once, and every new service — Ollama, a VDI, WordPress — joins as another peer with its own <code>100.x</code> address, reachable privately with <strong>no public ports to open</strong>. You set up the auth model once; you add services forever.</p>\n<!-- -->\n<p>The public internet only ever reaches <strong>NetBird's control plane</strong> — and that just\n<em>authenticates</em> peers, it never carries your app traffic. Your services aren't\n\"firewalled off\" so much as <strong>invisible</strong>: OpenClaw has no public IP, so there's\nsimply <strong>no route</strong> to it from the internet. The only way in is to be an\nauthenticated peer on the mesh.</p>\n<p>In this tutorial we wire up the first two peers — <strong>your laptop</strong> and the\n<strong>OpenClaw box</strong>. The other services make the point: once auth is set up,\n<code>Ollama</code>, a <code>VDI</code>, <code>WordPress</code> each just join the same mesh as another <code>100.x</code>\npeer — no public ports, no per-service exposure.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"stand-up-a-netbird-control-plane\">Stand up a NetBird control plane<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#stand-up-a-netbird-control-plane\" class=\"hash-link\" aria-label=\"Direct link to Stand up a NetBird control plane\" title=\"Direct link to Stand up a NetBird control plane\" translate=\"no\">​</a></h2>\n<p>Every NetBird mesh has two roles: <strong>peers</strong> — your machines, i.e. the OpenClaw box\nfrom Parts 1–3 and your laptop — and a <strong>control plane</strong> that authenticates those\npeers and issues their join keys. You already have the peer (your OpenClaw box);\nnow you need a control plane for it to join. Two ways to get one:</p>\n<ul>\n<li class=\"\"><strong>Option A — self-host it on WiLine (recommended).</strong> Deploy WEC's <strong>NetBird Marketplace template</strong>: a <strong>dedicated WEC Instance</strong> that runs your <em>own</em> NetBird control plane — dashboard, trusted certificate, zero shell. Now your <em>entire</em> private network lives on WiLine — the OpenClaw box <strong>and</strong> its control plane — with no third-party dependency.</li>\n<li class=\"\"><strong>Option B — managed NetBird (no extra box).</strong> Don't want to run your own control plane? Use the free tier at <a href=\"https://app.netbird.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">app.netbird.io</a> — sign in and you're done; the control plane is NetBird's cloud. Then skip to <a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#now-enroll-your-machines\" class=\"\">Enroll your machines</a>.</li>\n</ul>\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Two boxes, on purpose — and your OpenClaw box is untouched</div><div class=\"admonitionContent_BuS1\"><p>With <strong>Option A</strong> you'll run <strong>two instances</strong>: your <strong>OpenClaw box</strong> (Parts 1–3,\nunchanged) and a <strong>second, dedicated control-plane box</strong> (this template). That's the\ncorrect topology — a control plane should be its own machine, never sharing with a\nworkload. With <strong>Option B</strong> there's only your one OpenClaw box. <strong>Either way you\nnever rebuild the OpenClaw box</strong> — in <a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#now-enroll-your-machines\" class=\"\">Enroll your machines</a>\nyou just install the NetBird agent on it and it joins the mesh as a peer.</p></div></div>\n<p>The steps below set up <strong>Option A</strong>. WEC runs the install; you give it an email and a\npublic IP, and a few minutes later you have your own NetBird dashboard on\n<code>https://&lt;your-ip&gt;.apps.wiline.cloud</code> with a valid certificate.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"1-get-a-public-elastic-ip\">1. Get a public (Elastic) IP<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#1-get-a-public-elastic-ip\" class=\"hash-link\" aria-label=\"Direct link to 1. Get a public (Elastic) IP\" title=\"Direct link to 1. Get a public (Elastic) IP\" translate=\"no\">​</a></h3>\n<p>The dashboard needs a public address. Under <strong>Networks → Elastic IPs</strong>, allocate a\nnew IP or pick a free one, and note its value (ours: <code>108.60.112.165</code>). It must be\nin the <strong>VPC and zone you'll deploy into</strong>. Full steps:\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/networks/elastic_ip/\">Configure Elastic IPs</a>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"An available Elastic IP in Networks → Elastic IPs\" src=\"https://development-wec.wiline.com/docs/assets/images/netbird-eip-available-b4ec56b4c4946226b7c9e199bd540f61.png\" width=\"2793\" height=\"1305\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"2-deploy-a-vm-with-the-template\">2. Deploy a VM with the template<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#2-deploy-a-vm-with-the-template\" class=\"hash-link\" aria-label=\"Direct link to 2. Deploy a VM with the template\" title=\"Direct link to 2. Deploy a VM with the template\" translate=\"no\">​</a></h3>\n<p>This new instance becomes your <strong>NetBird control plane</strong> — a separate box from your\nOpenClaw instance, which stays untouched. Open <strong>Deploy → Deploy a VM</strong> (full walkthrough:\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/compute/instances/compute_instance/\">Deploy a Virtual Machine</a>).\nThe choices that matter here:</p>\n<ul>\n<li class=\"\"><strong>Network</strong> — pick the network in the <strong>same VPC/zone as your Elastic IP</strong> (we used <code>Local Subnet</code> in <code>Princeton-VPC</code>), so the IP can attach afterward.</li>\n<li class=\"\"><strong>Template → Marketplace → Ubuntu 24.04 LTS Netbird.</strong></li>\n<li class=\"\"><strong>Template Configuration</strong> — your <strong>Let's Encrypt email</strong> and the <strong>Elastic IP from step 1</strong> as the external address.</li>\n</ul>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Template Configuration — Let&amp;#39;s Encrypt email and the Elastic IP\" src=\"https://development-wec.wiline.com/docs/assets/images/netbird-template-config-e03939727bf27b7f6ce4ceb4fc5acdf1.png\" width=\"1968\" height=\"1240\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Finish the wizard and deploy.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"3-attach-the-elastic-ip-to-the-new-instance\">3. Attach the Elastic IP to the new instance<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#3-attach-the-elastic-ip-to-the-new-instance\" class=\"hash-link\" aria-label=\"Direct link to 3. Attach the Elastic IP to the new instance\" title=\"Direct link to 3. Attach the Elastic IP to the new instance\" translate=\"no\">​</a></h3>\n<p>The IP isn't bound at deploy time — you attach it once the VM is up. Go to\n<strong>Networks → Elastic IPs → your IP → ⋯ → Allocate</strong>, and select your new instance\n(<code>netbird-tutorial</code>). Only VMs in the <strong>same VPC</strong> appear in the list.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Allocating the Elastic IP to the new VM\" src=\"https://development-wec.wiline.com/docs/assets/images/netbird-eip-allocate-620e7b57084f55e9e8ca3323494cda6d.png\" width=\"1834\" height=\"1070\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"4-wait-for-first-boot-then-verify-its-up\">4. Wait for first boot, then verify it's up<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#4-wait-for-first-boot-then-verify-its-up\" class=\"hash-link\" aria-label=\"Direct link to 4. Wait for first boot, then verify it's up\" title=\"Direct link to 4. Wait for first boot, then verify it's up\" translate=\"no\">​</a></h3>\n<p>The instance shows <strong>Running</strong> in <strong>Compute → Instances</strong> within a minute — but the\ntemplate keeps working for a few minutes after that, pulling and starting the\nNetBird containers and bringing up the dashboard. Don't expect the URL to answer\nimmediately.</p>\n<p>Check from your <strong>laptop</strong> over HTTPS (not <code>ping</code> — ICMP is firewalled off, so a\nfailed ping doesn't mean it's down):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-skS</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">10</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> /dev/null </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-w</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"%{http_code}</span><span class=\"token string entity\" style=\"color:#36acaa\">\\n</span><span class=\"token string\" style=\"color:#e3116c\">\"</span><span class=\"token plain\"> https://</span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">your-ip</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">.apps.wiline.cloud</span><br></div></code></pre></div></div>\n<ul>\n<li class=\"\"><code>000</code> / connection refused → still provisioning; wait a minute and retry.</li>\n<li class=\"\"><code>301</code> then <code>200</code> → it's up (HTTP redirects to HTTPS).</li>\n</ul>\n<p>To watch it directly, SSH in (<code>ssh ubuntu@&lt;your-ip&gt;</code>) and confirm the containers are\nrunning and the ports are bound:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">cloud-init status</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> ss </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-tlnp</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-E</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">':(80|443)'</span><br></div></code></pre></div></div>\n<p>It's ready once <strong><code>netbird-dashboard</code>, <code>netbird-traefik</code>, and <code>netbird-server</code></strong> show\n<strong>Up</strong> and <strong>80/443</strong> are listening.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"5-open-the-dashboard-and-create-your-account\">5. Open the dashboard and create your account<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#5-open-the-dashboard-and-create-your-account\" class=\"hash-link\" aria-label=\"Direct link to 5. Open the dashboard and create your account\" title=\"Direct link to 5. Open the dashboard and create your account\" translate=\"no\">​</a></h3>\n<p>Open <strong><code>https://&lt;your-ip&gt;.apps.wiline.cloud</code></strong> — click through the certificate\nwarning if you get one (same self-signed-cert click-through as\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/\">Article 2</a>).</p>\n<p>This is <strong>your own</strong> NetBird control plane, running on your WEC Instance. On the\nfirst visit it asks you to <strong>create the admin account</strong> — enter a <strong>name, email,\nand password</strong>, click <strong>Create</strong>, then <strong>sign in</strong> with those same credentials.\n(That setup screen appears only once; afterwards the dashboard shows a normal\n<strong>Sign in</strong> page.)</p>\n<p>You land on the NetBird dashboard — your private control plane on your own domain:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The self-hosted NetBird dashboard\" src=\"https://development-wec.wiline.com/docs/assets/images/netbird-dashboard-c98eca329c445df75fee98ca55f230e9.png\" width=\"2850\" height=\"1646\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Click <strong>Add Peer</strong> and NetBird hands you the enrollment command for <em>your</em> server —\nnote the <code>--management-url</code> pointing at your own domain:</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Add Peer on the self-hosted dashboard — note the --management-url flag\" src=\"https://development-wec.wiline.com/docs/assets/images/netbird-add-peer-09fd78007bcc0279cae91217c1786221.png\" width=\"2827\" height=\"1599\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-fsSL</span><span class=\"token plain\"> https://pkgs.netbird.io/install.sh </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">netbird up --management-url https://</span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">your-ip</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">.apps.wiline.cloud</span><br></div></code></pre></div></div>\n<p>(That second command opens a browser SSO login. For a <strong>headless</strong> server, add\n<code>--setup-key &lt;KEY&gt;</code> from your dashboard's <strong>Setup Keys</strong> page instead — same as the\nmanaged flow, just with <code>--management-url</code> added.)</p>\n<p>This is the dashboard where you create the setup keys that enroll machines — which\nis exactly what we do next.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"now-enroll-your-machines\">Now enroll your machines<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#now-enroll-your-machines\" class=\"hash-link\" aria-label=\"Direct link to Now enroll your machines\" title=\"Direct link to Now enroll your machines\" translate=\"no\">​</a></h2>\n<p>You have a control plane (your own template dashboard, or managed NetBird). Now put\nthe two machines on the mesh — the <strong>OpenClaw box</strong> from Parts 1–3 and your\n<strong>laptop</strong> — and then close the box's public ports. These steps are the same no\nmatter which control plane you picked.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Where your setup key comes from</div><div class=\"admonitionContent_BuS1\"><p>The commands below use <strong>managed NetBird</strong> (<code>app.netbird.io</code>) as the example. If you\ndeployed the <strong>WEC template</strong> instead, it's identical except: create the setup key\nin <strong>your own</strong> dashboard (<code>https://&lt;your-ip&gt;.apps.wiline.cloud</code>) and add\n<strong><code>--management-url https://&lt;your-ip&gt;.apps.wiline.cloud</code></strong> to <code>netbird up</code>, so the\nagent points at <em>your</em> server instead of the managed cloud.</p></div></div>\n<p><strong>Prerequisites:</strong> OpenClaw running behind Caddy from\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/\">Articles 1–3</a> (reachable at\n<code>https://openclaw.local</code>), a control plane from the step above, and shell access to\nthe box plus admin on your laptop.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--install-the-netbird-agent-on-the-openclaw-box\">Step 1 — Install the NetBird agent on the OpenClaw box<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#step-1--install-the-netbird-agent-on-the-openclaw-box\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Install the NetBird agent on the OpenClaw box\" title=\"Direct link to Step 1 — Install the NetBird agent on the OpenClaw box\" translate=\"no\">​</a></h2>\n<p>This is the box from Parts 1–3 (a WEC Instance) — we're enrolling it as a <strong>peer</strong>,\nwhich is separate from any control-plane server. NetBird ships a one-line installer:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-fsSL</span><span class=\"token plain\"> https://pkgs.netbird.io/install.sh </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><br></div></code></pre></div></div>\n<p><strong>Gotcha (real one):</strong> on Ubuntu the install triggers the <code>needrestart</code> dialog —\na blue <em>\"Which services should be restarted?\"</em> box. <code>Tab</code> between buttons can get\nswallowed by some SSH/terminal setups; use <strong>arrow keys</strong> to move, <strong>Space</strong> to\ntoggle, <strong>Enter</strong> to confirm (<code>&lt;Ok&gt;</code> is the default). The package is already in\nplace by the time this box appears, so even if you <code>Ctrl+C</code> out of it, the agent\nis installed — re-running the installer just tells you so:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">NetBird seems to be installed already, please remove it before proceeding</span><br></div></code></pre></div></div>\n<p>(If you re-run the installer after this tutorial, you'll instead see\n<code>NetBird service is running, please stop it before proceeding</code> — the agent's\nalready up, so there's nothing to reinstall.)</p>\n<p>Confirm the version:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">netbird version</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 0.73.2</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--create-a-setup-key-and-join-the-openclaw-box\">Step 2 — Create a setup key and join the OpenClaw box<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#step-2--create-a-setup-key-and-join-the-openclaw-box\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Create a setup key and join the OpenClaw box\" title=\"Direct link to Step 2 — Create a setup key and join the OpenClaw box\" translate=\"no\">​</a></h2>\n<p>A <strong>setup key</strong> is the token that enrolls a device into your mesh — ideal for a\nheadless server (no browser SSO needed).</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>First time in the dashboard? Skip the detours</div><div class=\"admonitionContent_BuS1\"><p>A few things will try to send you the wrong way on a fresh account — here's the\nstraight line:</p><ul>\n<li class=\"\"><strong>No account yet?</strong> Create one first (sign in with Google/GitHub/email). You can't generate a key until you're in.</li>\n<li class=\"\"><strong>The onboarding wizard</strong> (\"Add your first resource\" — <em>Single IP / Entire Subnet / Domain</em>) pops up immediately. <strong>Skip it.</strong> Resources are for exposing subnets/domains; we just need two plain peers, so it's not needed.</li>\n<li class=\"\"><strong>The orange \"Add Peer\" button</strong> opens the <strong>SSO install flow</strong> (download the app, sign up with email, connect from the tray). That's meant for a desktop with a browser — <strong>not</strong> a headless server. Don't follow it.</li>\n<li class=\"\">Instead, go straight to the <strong>Setup Keys</strong> page: <strong><code>https://app.netbird.io/setup-keys</code></strong>.</li>\n</ul></div></div>\n<ol>\n<li class=\"\">Open <strong><code>https://app.netbird.io/setup-keys</code></strong>.</li>\n<li class=\"\"><strong>Create Setup Key</strong> → name <code>openclaw-tutorial</code>, toggle <strong>Reusable</strong> (so it works for the laptop too), defaults otherwise.</li>\n<li class=\"\">Copy the key.</li>\n</ol>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Creating a reusable setup key in the NetBird dashboard\" src=\"https://development-wec.wiline.com/docs/assets/images/netbird-setup-key-ade9af748bded2998a18890a1ebde93b.png\" width=\"1898\" height=\"950\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Connect the VM (replace with your key):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> netbird up --setup-key </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">YOUR_SETUP_KEY</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Treat the setup key as a secret</div><div class=\"admonitionContent_BuS1\"><p>Anyone with a reusable key can add devices to your network. Don't paste it into\nchats or commits, and <strong>delete it</strong> from the dashboard once your devices are\nenrolled (the existing peers stay connected).</p></div></div>\n<p>Check the connection:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">netbird status</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"netbird status on the VM — Management &amp;amp; Signal Connected, mesh IP 100.87.239.229, Peers count 0/0\" src=\"https://development-wec.wiline.com/docs/assets/images/netbird-vm-status-8e8088e406d7392203f1eb5da63b4258.png\" width=\"731\" height=\"374\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>That <strong><code>NetBird IP</code> (<code>100.87.239.229</code>)</strong> is the address everything below points\nat. <code>Peers count: 0/0</code> is expected — nothing else has joined yet.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--join-your-laptop\">Step 3 — Join your laptop<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#step-3--join-your-laptop\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — Join your laptop\" title=\"Direct link to Step 3 — Join your laptop\" translate=\"no\">​</a></h2>\n<p>Install the NetBird client on whatever you're using, then connect with the\n<strong>same reusable setup key</strong>. The connect command (<code>netbird up --setup-key …</code>) is\nidentical on every OS — only the install differs.</p>\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>We ran this on macOS</div><div class=\"admonitionContent_BuS1\"><p>The captured output below is from a Mac. The Windows and Linux tabs give the\nequivalent install; the <code>netbird up</code>, <code>netbird status</code>, and ping steps are the\nsame everywhere (Windows uses <code>ping -n</code> instead of <code>-c</code>).</p></div></div>\n<div class=\"theme-tabs-container tabs-container tabList_zI5C\"><ul role=\"tablist\" aria-orientation=\"horizontal\" class=\"tabs\"><li role=\"tab\" tabindex=\"0\" aria-selected=\"true\" class=\"tabs__item tabItem_yeRP tabs__item--active\">macOS</li><li role=\"tab\" tabindex=\"-1\" aria-selected=\"false\" class=\"tabs__item tabItem_yeRP\">Windows</li><li role=\"tab\" tabindex=\"-1\" aria-selected=\"false\" class=\"tabs__item tabItem_yeRP\">Linux</li></ul><div class=\"margin-top--md\"><div role=\"tabpanel\" class=\"tabItem_yrzq\"><p>Install via Homebrew, then start the service and connect:</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">brew </span><span class=\"token function\" style=\"color:#d73a49\">install</span><span class=\"token plain\"> netbirdio/tap/netbird</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> netbird </span><span class=\"token function\" style=\"color:#d73a49\">service</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">install</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> netbird </span><span class=\"token function\" style=\"color:#d73a49\">service</span><span class=\"token plain\"> start</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">netbird up --setup-key </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">YOUR_SETUP_KEY</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><br></div></code></pre></div></div><p>You may see a one-off gRPC warning as the daemon's socket comes up a beat after\nthe client calls it — it ends in <code>Connected</code>, so it's harmless:</p><div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">WARNING: ... dial unix /var/run/netbird.sock: connect: no such file or directory</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Connected</span><br></div></code></pre></div></div></div><div role=\"tabpanel\" class=\"tabItem_yrzq\" hidden=\"\"><p>Download and run the <strong>NetBird Windows installer</strong> from\n<a href=\"https://app.netbird.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">app.netbird.io</a> (Add Peer → Windows) or\n<a href=\"https://pkgs.netbird.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">pkgs.netbird.io</a>. It installs a system-tray app and the\n<code>netbird</code> CLI. Then, in <strong>PowerShell as Administrator</strong>, connect with your key:</p><div class=\"language-powershell codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-powershell codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">netbird up --setup-key &lt;YOUR_SETUP_KEY&gt;</span><br></div></code></pre></div></div><p>(You can also click <strong>Connect</strong> from the tray icon and authenticate via SSO, but\nthe setup key is the same one-shot flow as the other platforms.)</p></div><div role=\"tabpanel\" class=\"tabItem_yrzq\" hidden=\"\"><p>Same one-line installer as the VM, then connect:</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-fsSL</span><span class=\"token plain\"> https://pkgs.netbird.io/install.sh </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> netbird up --setup-key </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">YOUR_SETUP_KEY</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><br></div></code></pre></div></div></div></div></div>\n<p>Verify the two nodes see each other, and that the tunnel carries traffic:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">netbird status</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">ping</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-c</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">100.87</span><span class=\"token plain\">.239.229   </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Windows: ping -n 3 100.87.239.229</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Mac netbird status showing Peers count 1/1 Connected, plus a successful ping to the VM&amp;#39;s mesh IP\" src=\"https://development-wec.wiline.com/docs/assets/images/netbird-mac-status-857d291924930e23af4e6550b1845ec7.png\" width=\"609\" height=\"475\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><code>Peers count: 1/1</code> — that peer is the VM. In the dashboard, both machines now\nshow up under <strong>Peers</strong> (because we enrolled them with a setup key rather than an\nSSO login, they appear under <strong>Servers</strong> rather than <strong>User Devices</strong>):</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The VM and laptop both connected as peers in the NetBird dashboard\" src=\"https://development-wec.wiline.com/docs/assets/images/netbird-peers-a54440db77e8715febacc8d410c24da5.png\" width=\"1895\" height=\"951\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Why ~200 ms?</div><div class=\"admonitionContent_BuS1\"><p>This path is going through a NetBird <strong>relay</strong> (note <code>Interface type: Userspace</code>\non the Mac). It's fine for a web UI, and NetBird will often upgrade to a direct\npeer-to-peer link when the networks allow. Latency to a relayed peer is not a\nred flag.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--repoint-openclawlocal-at-the-mesh-ip\">Step 4 — Repoint <code>openclaw.local</code> at the mesh IP<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#step-4--repoint-openclawlocal-at-the-mesh-ip\" class=\"hash-link\" aria-label=\"Direct link to step-4--repoint-openclawlocal-at-the-mesh-ip\" title=\"Direct link to step-4--repoint-openclawlocal-at-the-mesh-ip\" translate=\"no\">​</a></h2>\n<p>From <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/\">Article 2</a>, your hosts file maps\n<code>openclaw.local</code> to the VM's LAN IP (<code>10.80.4.212</code>). Swap it for the mesh IP\n(<code>100.87.239.229</code>). The file path and edit command differ by OS:</p>\n<div class=\"theme-tabs-container tabs-container tabList_zI5C\"><ul role=\"tablist\" aria-orientation=\"horizontal\" class=\"tabs\"><li role=\"tab\" tabindex=\"0\" aria-selected=\"true\" class=\"tabs__item tabItem_yeRP tabs__item--active\">macOS</li><li role=\"tab\" tabindex=\"-1\" aria-selected=\"false\" class=\"tabs__item tabItem_yeRP\">Linux</li><li role=\"tab\" tabindex=\"-1\" aria-selected=\"false\" class=\"tabs__item tabItem_yeRP\">Windows</li></ul><div class=\"margin-top--md\"><div role=\"tabpanel\" class=\"tabItem_yrzq\"><p><code>sed</code> on macOS needs the empty <code>-i ''</code>:</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> openclaw /etc/hosts</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 10.80.4.212 openclaw.local</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">''</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/^10\\.80\\.4\\.212 openclaw\\.local/100.87.239.229 openclaw.local/'</span><span class=\"token plain\"> /etc/hosts</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> openclaw /etc/hosts</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 100.87.239.229 openclaw.local</span><br></div></code></pre></div></div></div><div role=\"tabpanel\" class=\"tabItem_yrzq\" hidden=\"\"><p>GNU <code>sed</code> takes <code>-i</code> with no argument:</p><div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> openclaw /etc/hosts</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sed</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-i</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">'s/^10\\.80\\.4\\.212 openclaw\\.local/100.87.239.229 openclaw.local/'</span><span class=\"token plain\"> /etc/hosts</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> openclaw /etc/hosts</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># 100.87.239.229 openclaw.local</span><br></div></code></pre></div></div></div><div role=\"tabpanel\" class=\"tabItem_yrzq\" hidden=\"\"><p>The hosts file is at <code>C:\\Windows\\System32\\drivers\\etc\\hosts</code> and needs admin.\nIn <strong>PowerShell as Administrator</strong>:</p><div class=\"language-powershell codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-powershell codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">$h = \"$env:WINDIR\\System32\\drivers\\etc\\hosts\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">(Get-Content $h) -replace '^10\\.80\\.4\\.212 openclaw\\.local','100.87.239.229 openclaw.local' | Set-Content $h</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Select-String openclaw $h</span><br></div></code></pre></div></div><p>(Or open that file in <strong>Notepad run as Administrator</strong> and change the IP by hand.)</p></div></div></div>\n<p>Test over the mesh — <em>before</em> touching any ports:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-kI</span><span class=\"token plain\"> https://openclaw.local/</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">HTTP/2 200</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">...</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">via: 1.1 Caddy</span><br></div></code></pre></div></div>\n<p><code>via: 1.1 Caddy</code> over the mesh IP — the chat now flows through NetBird. Load\n<code>https://openclaw.local/chat?session=main</code> in the browser to confirm it behaves\nexactly as before. The TLS cert still matches because the hostname is unchanged;\nonly the IP behind it moved.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The OpenClaw chat loading over the mesh at https://openclaw.local\" src=\"https://development-wec.wiline.com/docs/assets/images/netbird-chat-mesh-409321edd81ea8d59656891246bc0558.png\" width=\"1900\" height=\"993\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--close-the-public-ports\">Step 5 — Close the public ports<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#step-5--close-the-public-ports\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — Close the public ports\" title=\"Direct link to Step 5 — Close the public ports\" translate=\"no\">​</a></h2>\n<p>Here's the state we're fixing. On the VM:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> ss </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-tlnp</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-E</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">':(80|443)'</span><br></div></code></pre></div></div>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockTitle_OeMC\">Output — listening on 0.0.0.0 (every interface)</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">LISTEN 0 4096  0.0.0.0:443  ...  docker-proxy</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">LISTEN 0 4096  0.0.0.0:80   ...  docker-proxy</span><br></div></code></pre></div></div>\n<p>Caddy listens on <strong><code>0.0.0.0</code></strong> — every interface, including the LAN (and any NAT\nin front of it).</p>\n<div class=\"theme-admonition theme-admonition-danger admonition_xJq3 alert alert--danger\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z\"></path></svg></span>The ufw trap</div><div class=\"admonitionContent_BuS1\"><p>Your first instinct is <code>ufw deny 443</code>. <strong>It won't work.</strong> Caddy's ports are\npublished by <strong>Docker</strong>, which writes its own iptables rules that bypass ufw's\n<code>INPUT</code> chain. You'd add ufw rules, feel safe, and 443 would stay wide open. On\nthis box <code>ufw</code> was even <code>inactive</code> — and enabling it would have changed nothing\nfor the Docker-published ports.</p></div></div>\n<p>The reliable fix is to <strong>bind the published ports to the mesh IP</strong> instead of\n<code>0.0.0.0</code>, so Caddy only ever listens on <code>wt0</code>. Edit the Caddy service in\n<code>docker-compose.override.yml</code> (the override from Article 2):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> /home/ubuntu/openclaw</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">cp</span><span class=\"token plain\"> docker-compose.override.yml docker-compose.override.yml.bak</span><br></div></code></pre></div></div>\n<p>Change the ports block from:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">ports</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"80:80\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"443:443\"</span><br></div></code></pre></div></div>\n<p>to bind to the mesh IP:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">    </span><span class=\"token key atrule\" style=\"color:#00a4db\">ports</span><span class=\"token punctuation\" style=\"color:#393A34\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"100.87.239.229:80:80\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:#393A34\">-</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"100.87.239.229:443:443\"</span><br></div></code></pre></div></div>\n<p>Recreate just Caddy and re-check:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> caddy</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> ss </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-tlnp</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">grep</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-E</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">':(80|443)'</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"ss after the change — Caddy now listening only on the mesh IP 100.87.239.229, no longer on 0.0.0.0\" src=\"https://development-wec.wiline.com/docs/assets/images/netbird-ss-after-0172e6fc62bc667a141b1f6528526e73.png\" width=\"745\" height=\"103\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>The ports are gone from <code>0.0.0.0</code> — they live on the mesh interface only.</p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Startup ordering</div><div class=\"admonitionContent_BuS1\"><p>Because the bind targets the mesh IP, <strong>NetBird must be up before Caddy starts</strong>\n(e.g. after a reboot, <code>wt0</code> needs its IP before Docker binds to it). The\n<code>restart: unless-stopped</code> policy already covers this — Caddy retries until the\ninterface exists — but it's worth knowing if you ever see Caddy crash-looping\nright after boot.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-6--prove-the-lockdown\">Step 6 — Prove the lockdown<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#step-6--prove-the-lockdown\" class=\"hash-link\" aria-label=\"Direct link to Step 6 — Prove the lockdown\" title=\"Direct link to Step 6 — Prove the lockdown\" translate=\"no\">​</a></h2>\n<p>Both directions, from the actual run.</p>\n<p><strong>Still reachable over the mesh</strong> (laptop):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-kI</span><span class=\"token plain\"> https://openclaw.local/</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># HTTP/2 200 ... via: 1.1 Caddy</span><br></div></code></pre></div></div>\n<p><strong>No longer reachable on the LAN</strong> (VM, hitting its own LAN IP):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-kI</span><span class=\"token plain\"> --connect-timeout </span><span class=\"token number\" style=\"color:#36acaa\">5</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--resolve</span><span class=\"token plain\"> openclaw.local:443:10.80.4.212 https://openclaw.local/</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"On the VM, curl to the old LAN IP now returns Connection refused\" src=\"https://development-wec.wiline.com/docs/assets/images/netbird-refused-0944b25666489470ae7b961e2d6243c8.png\" width=\"617\" height=\"55\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>That's the result: <strong>same URL, reachable only through your mesh, refused\neverywhere else.</strong></p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can now <strong>take any self-hosted service off the public internet</strong> without breaking how you\nuse it.</p></div></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"cleanup\">Cleanup<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#cleanup\" class=\"hash-link\" aria-label=\"Direct link to Cleanup\" title=\"Direct link to Cleanup\" translate=\"no\">​</a></h2>\n<p>Delete the reusable setup key now that both devices are enrolled —\n<strong>Setup Keys → delete</strong> in the dashboard. Your peers stay connected; you're just\nclosing the door you used to add them. To remove a device later, delete it under\n<strong>Peers → User Devices</strong> and run <code>netbird down</code> on that machine.</p>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting-real\">Troubleshooting (real)<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#troubleshooting-real\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting (real)\" title=\"Direct link to Troubleshooting (real)\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong><code>needrestart</code> dialog won't take <code>Tab</code>.</strong> Use arrow keys / Space / Enter. The package is already installed by the time the box shows.</li>\n<li class=\"\"><strong>gRPC <code>netbird.sock</code> warning on first <code>up</code>.</strong> The daemon's socket lags the client by a beat. If it ends in <code>Connected</code>, ignore it.</li>\n<li class=\"\"><strong><code>ufw</code> rules don't block the gateway.</strong> Docker-published ports bypass ufw — bind the port to a specific IP (as above) or use Docker's own <code>DOCKER-USER</code> chain.</li>\n<li class=\"\"><strong>Caddy crash-loops after reboot.</strong> It started before <code>wt0</code> had the mesh IP. <code>restart: unless-stopped</code> recovers it; or order Docker after <code>netbird.service</code>.</li>\n<li class=\"\"><strong>High ping to a peer.</strong> You're relayed, not direct — functional, just slower. Check <code>Interface type</code> and NetBird's peer detail for the connection type.</li>\n</ul>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Private mesh access</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>That closes the <strong>Self-hosting OpenClaw</strong> series:</p>\n<ol>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\">Deploy OpenClaw on a WEC Instance via Docker Compose</a></li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/\">Secure OpenClaw with a Caddy reverse proxy + HTTPS</a></li>\n<li class=\"\"><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/\">Add a Telegram channel to OpenClaw</a></li>\n<li class=\"\"><strong>Make OpenClaw private with a NetBird mesh VPN</strong> ← you are here</li>\n</ol>\n<p>You now have a self-hosted agent that's encrypted in transit, paired to your\ndevices, reachable from your phone, and invisible to the public internet — all on\none small WEC Instance.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"continue-the-journey\">Continue the journey<a href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/#continue-the-journey\" class=\"hash-link\" aria-label=\"Direct link to Continue the journey\" title=\"Direct link to Continue the journey\" translate=\"no\">​</a></h3>\n<p>Ready for an agent that <em>remembers</em>? Start the <strong>Self-hosting Hermes</strong> series —\n<strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/\">Self-host the Hermes Agent with persistent memory</a></strong> —\ndeployed on this same WEC Instance, with context that survives restarts.</p>",
            "url": "https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/",
            "title": "Make OpenClaw private with a NetBird mesh VPN",
            "summary": "Put OpenClaw on a private mesh with NetBird, repoint your hostname at the mesh IP, and close the public ports — so the same chat URL works only for your devices. Real commands and gotchas from a live run.",
            "date_modified": "2026-06-24T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "self-hosting",
                "openclaw",
                "netbird",
                "vpn",
                "mesh",
                "security"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/openclaw-wordmark-003352a1a7f02afc3bd877ae6f2dc175.png\" alt=\"OpenClaw\"><span class=\"tutorialHero__plus\">+</span><svg viewBox=\"0 0 24 24\" fill=\"#26A5E4\" aria-label=\"Telegram\" class=\"tutorialHero__docker\"><path d=\"M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 6 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->6</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->6<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting OpenClaw</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Deploy your own AI assistant</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real HTTPS + auth</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">3</span><span class=\"skillTracker__skill\" data-state=\"current\">Chat from Telegram</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Private mesh access</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/run-openclaw-on-wec-models/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run it on WEC models</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Chat from WhatsApp</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>You've <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\">deployed OpenClaw</a> and\n<a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/\">secured it behind HTTPS</a>. Now make it\n<em>usable</em> — talk to your agent from your phone via Telegram. We create a bot,\nconnect it, clear OpenClaw's pairing gate, and get a real reply. Every command and\ngotcha below is from an actual run.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>Continues from Parts 1–2 on the same WEC Instance — OpenClaw <strong>2026.6.8</strong>. Telegram\nuses <strong>polling</strong> (the gateway polls Telegram's API), so no public webhook or domain\nis required — this works on a private box.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-youll-build\">What you'll build<a href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/#what-youll-build\" class=\"hash-link\" aria-label=\"Direct link to What you'll build\" title=\"Direct link to What you'll build\" translate=\"no\">​</a></h2>\n<p>Your phone talks to a Telegram bot; the OpenClaw gateway polls Telegram for\nmessages, runs them through your model, and replies — no inbound ports needed.</p>\n<!-- -->\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\">Part 1</a></strong> + <strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/\">Part 2</a></strong> done — OpenClaw running in <code>~/openclaw</code></li>\n<li class=\"\">A <strong>Telegram account</strong> and a <strong>logged-in Telegram client</strong> — the phone/desktop app, or <code>web.telegram.org</code>. (You can't create a bot from the public <code>t.me</code> web page; see <a href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/#troubleshooting-real-errors\" class=\"\">Troubleshooting</a>.)</li>\n</ul>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--create-a-telegram-bot\">Step 1 — Create a Telegram bot<a href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/#step-1--create-a-telegram-bot\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Create a Telegram bot\" title=\"Direct link to Step 1 — Create a Telegram bot\" translate=\"no\">​</a></h2>\n<p>In the Telegram app, tap <strong>search</strong> and type <strong><code>BotFather</code></strong>, then open the official\naccount — the one with the <strong>blue checkmark</strong>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Searching for BotFather in Telegram\" src=\"https://development-wec.wiline.com/docs/assets/images/tg-step1a-search-922bcf2cacaf250ac10c1a03546f240b.png\" width=\"1100\" height=\"2381\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Tap <strong>Start</strong> — BotFather replies with the commands it understands (you'll see\noptions like <code>/newbot</code>, <code>/mybots</code>, <code>/token</code>, and more). Send <strong><code>/newbot</code></strong>, then\nfollow its two prompts:</p>\n<ol>\n<li class=\"\"><strong>Name</strong> — a display name, e.g. <code>My OpenClaw Agent</code></li>\n<li class=\"\"><strong>Username</strong> — must be unique and end in <code>bot</code>, e.g. <code>wiline_openclaw_bot</code></li>\n</ol>\n<p>BotFather confirms with <em>\"Done! Congratulations on your new bot\"</em> and gives you the\n<strong>token</strong> — the line after <code>Use this token to access the HTTP API:</code>, like\n<code>1234567890:AA...</code>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"BotFather issuing the bot token\" src=\"https://development-wec.wiline.com/docs/assets/images/tg-step1c-token-0bec47ec554ad66500f6d053bd0a4810.png\" width=\"756\" height=\"1316\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>The bot token is a credential</div><div class=\"admonitionContent_BuS1\"><p>Anyone with it controls your bot. <strong>Blur it in the screenshot above</strong>, and <code>/revoke</code>\nit in BotFather if it ever leaks.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--connect-the-bot-to-openclaw\">Step 2 — Connect the bot to OpenClaw<a href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/#step-2--connect-the-bot-to-openclaw\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Connect the bot to OpenClaw\" title=\"Direct link to Step 2 — Connect the bot to OpenClaw\" translate=\"no\">​</a></h2>\n<p>On the <strong>VM</strong>, in <code>~/openclaw</code>, register the channel with your token:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli channels </span><span class=\"token function\" style=\"color:#d73a49\">add</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--channel</span><span class=\"token plain\"> telegram </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--token</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"&lt;your-bot-token&gt;\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Added Telegram account \"default\".</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"channels add succeeding\" src=\"data:image/png;base64,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\" width=\"641\" height=\"242\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--activate-and-verify\">Step 3 — Activate and verify<a href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/#step-3--activate-and-verify\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — Activate and verify\" title=\"Direct link to Step 3 — Activate and verify\" translate=\"no\">​</a></h2>\n<p>Restart the gateway so it picks up the channel, then check status:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose restart openclaw-gateway</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli channels status</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># - Telegram default: enabled, configured, running, mode:polling, token:config</span><br></div></code></pre></div></div>\n<p><code>mode:polling</code> means the gateway is actively polling Telegram — no inbound port or\nwebhook required.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"channels status showing Telegram running in polling mode\" src=\"https://development-wec.wiline.com/docs/assets/images/tg-step3-status-6393f1690ec9b270312c2f5e3a25f137.png\" width=\"642\" height=\"392\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--message-the-bot-pairing-gate\">Step 4 — Message the bot (pairing gate)<a href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/#step-4--message-the-bot-pairing-gate\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Message the bot (pairing gate)\" title=\"Direct link to Step 4 — Message the bot (pairing gate)\" translate=\"no\">​</a></h2>\n<p>Open your bot (<code>t.me/&lt;your-bot-username&gt;</code>), tap <strong>Start</strong>, and send a message.</p>\n<p>OpenClaw treats unknown senders as untrusted — so instead of replying, the bot\nreturns a <strong>pairing code</strong> and your Telegram user id. This is the same security\npattern as device pairing in Part 2.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Bot replying with the pairing code\" src=\"https://development-wec.wiline.com/docs/assets/images/tg-step4-pairing-94e777f1790fbeadab0cd42be384b80b.png\" width=\"1100\" height=\"2381\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--approve-the-pairing\">Step 5 — Approve the pairing<a href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/#step-5--approve-the-pairing\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — Approve the pairing\" title=\"Direct link to Step 5 — Approve the pairing\" translate=\"no\">​</a></h2>\n<p>On the <strong>VM</strong>, approve the code from the bot's message:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli pairing approve telegram </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">pairing-code</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Approved telegram sender &lt;id&gt;.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># Command owner configured telegram:&lt;id&gt; (commands.ownerAllowFrom was empty).</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>note</div><div class=\"admonitionContent_BuS1\"><p>Approving the first user as <strong>command owner</strong> changes config that requires a\ngateway restart — OpenClaw does this <strong>automatically</strong> (you'll see\n<code>config change requires gateway restart</code> then the Telegram provider restart in the\nlogs). Wait a few seconds for it to come back before messaging again.</p></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Approving the pairing on the VM — &amp;quot;Approved telegram sender …&amp;quot;\" src=\"data:image/png;base64,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\" width=\"639\" height=\"240\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-6--chat-with-your-agent\">Step 6 — Chat with your agent<a href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/#step-6--chat-with-your-agent\" class=\"hash-link\" aria-label=\"Direct link to Step 6 — Chat with your agent\" title=\"Direct link to Step 6 — Chat with your agent\" translate=\"no\">​</a></h2>\n<p>Back in Telegram, just talk to it — no extra setup needed; OpenClaw with your model\nanswers conversationally out of the box. Try something like:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">In one sentence, what's the point of putting a reverse proxy in front of my agent?</span><br></div></code></pre></div></div>\n<p>It replies with a real answer — the full chain is live: your phone → Telegram →\ngateway → OpenAI → reply. Your self-hosted agent is now in your pocket. (Giving it\ntools and skills comes in later guides; conversational chat works immediately.)</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The agent replying conversationally over Telegram\" src=\"https://development-wec.wiline.com/docs/assets/images/tg-step6-working-7acac4effbd1c03d0657534125e3ea3b.png\" width=\"1100\" height=\"2381\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>Your agent is now <strong>in your pocket</strong> — a private Telegram line to your own AI, no public\nwebhook needed.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting-real-errors\">Troubleshooting (real errors)<a href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/#troubleshooting-real-errors\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting (real errors)\" title=\"Direct link to Troubleshooting (real errors)\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"1-botfather-start-does-nothing-in-a-browser\">1. BotFather \"Start\" does nothing in a browser<a href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/#1-botfather-start-does-nothing-in-a-browser\" class=\"hash-link\" aria-label=\"Direct link to 1. BotFather &quot;Start&quot; does nothing in a browser\" title=\"Direct link to 1. BotFather &quot;Start&quot; does nothing in a browser\" translate=\"no\">​</a></h3>\n<p>Opening <code>t.me/BotFather</code> in a plain web browser shows a <strong>Start</strong> button that does\nnothing — that page is just the public bot card and needs a <strong>logged-in Telegram\nclient</strong> to hand off to. Use the desktop/phone app, or log in at\n<code>web.telegram.org</code> (link a desktop device by scanning the QR from your phone:\nSettings → Devices → Link Desktop Device), then message BotFather there.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"2-bot-returns-a-pairing-code-instead-of-replying\">2. Bot returns a pairing code instead of replying<a href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/#2-bot-returns-a-pairing-code-instead-of-replying\" class=\"hash-link\" aria-label=\"Direct link to 2. Bot returns a pairing code instead of replying\" title=\"Direct link to 2. Bot returns a pairing code instead of replying\" translate=\"no\">​</a></h3>\n<p>Not an error — it's the <strong>pairing gate</strong>. Unknown senders must be approved first\n(Step 5). After approval, the bot chats normally.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"3-proxy-headers-detected-from-untrusted-address\">3. <code>Proxy headers detected from untrusted address</code><a href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/#3-proxy-headers-detected-from-untrusted-address\" class=\"hash-link\" aria-label=\"Direct link to 3-proxy-headers-detected-from-untrusted-address\" title=\"Direct link to 3-proxy-headers-detected-from-untrusted-address\" translate=\"no\">​</a></h3>\n<p>If you followed Part 2, the gateway logs this because Caddy sits in front of it.\nIt's a warning, not a failure. To silence it and restore correct client\ndetection, set <code>gateway.trustedProxies</code> to your proxy's network.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"security--sizing-notes\">Security &amp; sizing notes<a href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/#security--sizing-notes\" class=\"hash-link\" aria-label=\"Direct link to Security &amp; sizing notes\" title=\"Direct link to Security &amp; sizing notes\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>Pairing is the gate</strong> — only approved Telegram users reach the agent; the first\napproved user becomes the command owner.</li>\n<li class=\"\"><strong>Polling, not webhooks</strong> — no inbound port is opened, which keeps the Part 2\nhardening intact.</li>\n<li class=\"\"><strong>Sizing</strong> — on a 2-vCPU box the gateway logged <code>event loop degraded</code> under load.\nFor a busy bot, give it more CPU/RAM.</li>\n</ul>\n<hr>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Chat from Telegram</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>You've finished <strong>Part 3</strong> of the <strong>Self-hosting OpenClaw</strong> series. Next:</p>\n<ul>\n<li class=\"\"><strong>Part 4 — <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/\">Make OpenClaw private with a NetBird mesh VPN</a></strong> <em>(next)</em> — put your agent on a private mesh and close the public ports.</li>\n</ul>\n<p>Then start the <strong>Self-hosting Hermes</strong> series — <strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/self-host-hermes-agent/\">Self-host the Hermes Agent with persistent memory</a></strong> — for an agent whose context survives restarts.</p>\n<p>Companion files for the series live in the WiLine manifests repo <em>(link coming with the repo)</em>.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"teardown\">Teardown<a href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/#teardown\" class=\"hash-link\" aria-label=\"Direct link to Teardown\" title=\"Direct link to Teardown\" translate=\"no\">​</a></h2>\n<p>Remove the Telegram channel (keeps the rest of OpenClaw running):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli channels remove </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--channel</span><span class=\"token plain\"> telegram</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose restart openclaw-gateway</span><br></div></code></pre></div></div>\n<p>And <code>/revoke</code> the bot token in BotFather if you're done with it.</p>",
            "url": "https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/",
            "title": "Add a Telegram channel to OpenClaw",
            "summary": "Talk to your self-hosted OpenClaw agent from your phone. Create a Telegram bot, connect it, clear the pairing gate, and chat — captured from a real run.",
            "date_modified": "2026-06-23T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "self-hosting",
                "openclaw",
                "telegram",
                "chatbot"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/openclaw-wordmark-003352a1a7f02afc3bd877ae6f2dc175.png\" alt=\"OpenClaw\"><span class=\"tutorialHero__plus\">+</span><svg viewBox=\"0 0 24 24\" fill=\"#1F88C0\" aria-label=\"Caddy\" class=\"tutorialHero__docker\"><path d=\"M11.094.47c-.842 0-1.696.092-2.552.288a11.37 11.37 0 0 0-4.87 2.423 10.632 10.632 0 0 0-2.36 2.826A10.132 10.132 0 0 0 .305 8.582c-.398 1.62-.4 3.336-.043 5.048.085.405.183.809.31 1.212a11.85 11.85 0 0 0 1.662 3.729 3.273 3.273 0 0 0-.086.427 3.323 3.323 0 0 0 2.848 3.71 3.279 3.279 0 0 0 1.947-.346c1.045.51 2.17.864 3.339 1.04a11.66 11.66 0 0 0 4.285-.155 11.566 11.566 0 0 0 4.936-2.485 10.643 10.643 0 0 0 2.352-2.894 11.164 11.164 0 0 0 1.356-4.424 11.214 11.214 0 0 0-.498-4.335c.175-.077.338-.175.486-.293a.444.444 89.992 0 0 .001 0c.402-.322.693-.794.777-1.342a2.146 2.146 0 0 0-1.79-2.434 2.115 2.115 0 0 0-1.205.171c-.038-.043-.078-.086-.113-.13a11.693 11.693 0 0 0-3.476-2.93 13.348 13.348 0 0 0-1.76-.81 13.55 13.55 0 0 0-2.06-.613A12.121 12.121 0 0 0 11.093.47Zm.714.328c.345-.004.688.01 1.028.042a9.892 9.892 0 0 1 2.743.639c.984.39 1.89.958 2.707 1.632.803.662 1.502 1.45 2.091 2.328.026.039.048.08.07.12a2.12 2.12 0 0 0-.435 2.646c-.158.114-.97.692-1.634 1.183-.414.308-.733.557-.733.557l.581.68s.296-.276.665-.638c.572-.562 1.229-1.233 1.395-1.403a2.122 2.122 0 0 0 1.907.677 11.229 11.229 0 0 1-.013 4.046 11.41 11.41 0 0 1-1.475 3.897 12.343 12.343 0 0 1-2.079 2.587c-1.19 1.125-2.633 2.022-4.306 2.531a10.826 10.826 0 0 1-3.973.484 11.04 11.04 0 0 1-3.057-.652 3.304 3.304 0 0 0 1.417-2.294 3.275 3.275 0 0 0-.294-1.842c.18-.162.403-.363.656-.6 1.015-.955 2.353-2.303 2.353-2.303l-.47-.599s-1.63.972-2.801 1.728c-.307.198-.573.378-.777.517a3.273 3.273 0 0 0-1.516-.611c-1.507-.198-2.927.672-3.487 2.017a10.323 10.323 0 0 1-.695-1.078A10.92 10.92 0 0 1 .728 14.8a10.35 10.35 0 0 1-.2-1.212c-.164-1.653.103-3.258.629-4.754a12.95 12.95 0 0 1 1.087-2.288c.57-.968 1.248-1.872 2.069-2.656A11.013 11.013 0 0 1 11.808.797Zm-.147 3.257a3.838 3.838 0 0 0-3.82 3.82v2.36h-.94c-.751 0-1.377.625-1.377 1.377v3.8h1.46v-3.718h9.354v6.264H10.02v1.46h6.4c.751 0 1.377-.625 1.377-1.377v-6.43c0-.751-.626-1.377-1.377-1.377h-.94v-2.36a3.838 3.838 0 0 0-3.82-3.819zm0 1.46a2.371 2.371 0 0 1 2.36 2.36v2.36H9.3v-2.36a2.372 2.372 0 0 1 2.36-2.36zm10.141.392a1.253 1.253 0 0 1 1.296 1.434c-.049.319-.217.59-.453.78-.266.213-.61.318-.968.264a1.253 1.253 0 0 1-1.045-1.42 1.255 1.255 0 0 1 1.17-1.058zM5.384 17.425a2.02 2.02 0 0 1 1.917 1.298c.116.3.159.628.114.967a2.015 2.015 0 0 1-2.249 1.728 2.016 2.016 0 0 1-1.727-2.25 2.017 2.017 0 0 1 1.945-1.743z\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 6 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->6</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->6<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting OpenClaw</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\"><span class=\"skillTracker__dot\" data-state=\"locked\">1</span><span class=\"skillTracker__skill\" data-state=\"locked\">Deploy your own AI assistant</span></a></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">2</span><span class=\"skillTracker__skill\" data-state=\"current\">Real HTTPS + auth</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Chat from Telegram</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Private mesh access</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/run-openclaw-on-wec-models/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run it on WEC models</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Chat from WhatsApp</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>In <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\">Article 1</a> we got OpenClaw running —\nbut only over plain HTTP, with an <code>allowInsecureAuth</code> workaround. Here we put\n<a href=\"https://caddyserver.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Caddy</a> in front of it as a reverse proxy: real HTTPS,\ndevice-paired auth, and the gateway's raw ports closed so the proxy is the only\nway in. Every command and error below is from the actual deploy.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>Continues from Article 1 on the same WEC Instance — <strong>Ubuntu 22.04.5 LTS</strong>, OpenClaw\n<strong>2026.6.8</strong>. We use <strong>Caddy 2</strong> (<code>caddy:2</code>). Because this box has no public domain,\nwe use Caddy's <strong>internal CA</strong> (<code>tls internal</code>) for self-signed HTTPS; the\n<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#going-to-production\" class=\"\">Going to production</a> section shows the one-line swap to real\nLet's Encrypt certificates once you have a domain.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-youll-build\">What you'll build<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#what-youll-build\" class=\"hash-link\" aria-label=\"Direct link to What you'll build\" title=\"Direct link to What you'll build\" translate=\"no\">​</a></h2>\n<p>Caddy terminates HTTPS and reverse-proxies to the OpenClaw gateway over Docker's\ninternal network. The gateway stops publishing its own ports — the only entry\npoint becomes Caddy on <code>:443</code>.</p>\n<!-- -->\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\">Article 1</a> completed</strong> — OpenClaw running in <code>~/openclaw</code></li>\n<li class=\"\">Shell access to the VM</li>\n<li class=\"\">A hostname for the gateway. With a public domain, use it (and get real\nLet's Encrypt certs). On a private box, we use <code>openclaw.local</code> + a hosts entry.</li>\n</ul>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--confirm-the-starting-point\">Step 1 — Confirm the starting point<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#step-1--confirm-the-starting-point\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Confirm the starting point\" title=\"Direct link to Step 1 — Confirm the starting point\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> ~/openclaw</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><br></div></code></pre></div></div>\n<p>You should see <code>openclaw-gateway</code> <strong>Up (healthy)</strong>, publishing <code>18789/18790/3978</code>.\nThat's our starting state: working, but HTTP-only.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker compose ps showing the gateway running\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAjcAAACjCAMAAABBns/gAAAAXVBMVEURIECKjpp7gI4kMUvAwsYZJkRGT2Xd3d3T09SU21WnqrIADC6ZnKZ7tk9vdIRVXHA6Q1xla30vOlOztLuN0FNcY3VllUtTe0g5VkReoekPh8xHerVTjdApSHQ4YJQdmq0EAAAACXBIWXMAAAsTAAALEwEAmpwYAAAalklEQVR42u1dDZuqLBNGEb9ev1LTerfd//8znxkYEMxas/ZUu8x1neOKA8zAHRDeMex/XrzcLsyLFy9evHjx4uX3ymEQ3yu142FlcaTpXtbJMF60sSzLudXDwG4zaVm6ajF5Fy8VtF/RVE1V7f4CboZssVVLuxPLLMv6w1UVS7OcX9oyK1dYclHpAMVk5/WsdPCaEU2Rpt0SGNJgITFP+ffVVWmS/mHc2I1cZuNh7PsV/XzIykOZDe6Fq47bjptyycRyJW6uGSHSKM3DtbjZJftVNQa/GzdjBp+efhiyoc96QXejvOOHMSvHsaXEDD+y+P+ImkzAbT8YFZUIMwl0TJm10EG9exGjsLqsLyVYUX2YkkXZW/2qCmMcEwcwLRtHTnc4bkhFBzdOBrewuRH7NJwmniSPrHlKBFEK93CJknPc8DRnrJYadVR3aS4YL9IoZHG0z9Oc6+yEGw6lRFW0b6K9iJrfNtBk5QCN2mcD3Y2yu7EXECqUCAoiGxE9qAk9UQ7lpKISsacy6C94CjhyLu5QIgcPzJPZs08vq53usDChEuUslY10BxeoZZzhxsngFjY3IkyKKV+RJKF9V1RFxfKkCxfGmyJtWZNEYZTsdknSdQngIQ3ypK6TJCqSSmdXuBF5kocJlFMleZO0vxE3PeN4IdzA2IENnJWTihxIEDdSk2e9Na9QdiaxU/a9GrCci1EFsJUC5g0mx7gDDFoZJY5UpbqjwnQik1Mk3YGB46DWTsyU6WZwC5sZAWsamJfEx+mTsQ9AUTqNLDzJ8RIjsAxueFDEjMNcFiPCggRukm4Hmjwp4iSP90lQJ7BESgPKTripsdwk7PI8SXcp+424OTi4Gdk5btTgQJqj/igbFYmbnj73Pcsy96JVsZSew4VDN/aAPwAAJQ5UrVbpaVll4YbuBmmKwc1SBrewmRFKPo7H4+l0hL+iJNEzVazGniqpLdwUMGd1ecREFOHKOAHQRdEuaRA3VZKkaQq4iTFDrIcuiZsQE5MQpq4CxqbfhptSAWZQlxJBcZjhpsQZYBjH8kCa8/FGJpqeyw4CZwn7MlvywoiB+oQbs/Jm1mpHFyZrN7ghA4eFdbGTwS1s0YiPln2ecMhh0TRv0YCBPV6ZUSho2D7vaoUDOd6IFMabFlbJVZzIb9yEG3e8CQBaME8BtNrUnhl/w7pYLiZhzsCmpbsJN9Cro1EZ2Yh9ITVx+TDi7olUMYk9fWNWX6TsC+SFHNOXdhgG+Aw3BwmyGW5GnUjzlLwTuEguBwm1kS1mcAubG2Gvi+MoTFMz3sBoktfVvknyIJ2vixs1AMUwRxVJuEt2kLEVaVrvi4pwQ9kRN3UMmnmQ4FQWwbon/E24EWoxiQN/P+o7gxtMLimRtkBIk+HQb1R0In1NX9q/4XJq6a2BDrEACJhwg5rlHDdqtduzaQ7EO7lMHmY7OG4Gt7CZEfa6uOlSWOhO35jyNAHAFEl6ti7uIrV1A+uhpBCwLk46AEsDs1y0q2FoQdxQdqgBd3BgXRNGOYeEHU58v2qrWO1ccG7fGVgd2ilR4JYrTElcPxNaZUqktlebs+5ljS38soGzu0N73aPD4cBHa1KaGRE7hebO5q7ARTNrz22J9TdpgQ9hfUOFuKqiscpuBfNycYfwwrbhM6WU3/J/std2v+qL9Y8PT6VYmfhcaYeyL38UzU3gxxIvXrx48eLFi5ffJWf8pKrxjeLlWzl77/u79ja9eNx4eREhfpKiImnWEeCmiXJiJLGgCKPpfQ6pqAwF5K7gZTBRmKiUOgfiJdPZlRgV9czLm4viJxEVSbOOkjBO04YYSfiSpZiGJKVCGboEX86kRGHSpQCWqoDp7Hp3VarQMy9vLsRPIiqSZgEkeQqsRmIkAW52rAtdngFl6FJgWe5SojBRIkvTijOTXeNGqtAzL28uxE8iKpJmHSWSx0SMJJZ3DtBCNnGXOiCc1IgbSWGiRJiT4DWw0Nk1bqQKPfMN/+ZC/CSiIpnxBn4ZguwR9ZrYwY013kAGxE2MuJEUJkrE1UyX7HT26S3gTrJc8Jlv+DcX4icRFUmzjpKwAeAQI8nFDalQBoMbSWGixCbfN2Gy09k1bqQKPfMN/+5C/CRFRdKsI/j2BAtjoRhJLM/tDKSiMnSBULhRFCaV2EbwqzP46kTZzboYVfQzL+8uRDoS+mqzjpbIS1pFWM8MhYkSdSGLKo1HjZfVFCbPcvKyhcLkWU5evHjx4sWLFy9eXlnE2R/yBuRtPZoZT3/zunlzx66L8o/X7aVOFrp52KPeT0UFfk+GN0lRKjeEeYLSvOsOOMpOu6IcwxNFcLfynR27/mFRu7HwThDeAZLvbhNQT8N5K2m+e8D7qUhI3EA1eC5DDScDAW7yOI7f9HMpYnjfFnPtinIM/q8a+MH2OzvGrtNh0D+gINRwCA/57jYB9XQNP2zP7//NcYhvvgE3UD68lIwArABZngTvzQ2pmHFFOdaQR0nwW18zBuptM2zQRx357jYB9XQNmBFpcT9uupQDbvYJntoBHAf4oAJuuqravztulCvKMf2r/vd2jF3ZjZf+5XJoTcl3twmopxE3cEDCA3gUSQC4yVMkzkR5EiS4vkmjKH9z3JAryjGki+Apfu/t2HU6DCM8ADtX+X7WBNjTEjd5en+FAmmgcKZmnCcx0jzT6nfMU8qVVjm2B44Ifuh+6zy1V9QUiYc8It/dJqCelriJugfgBr6AFHv5RSOIwiAJfgtupCs75ViLbLNfjJtWs+kaWLzk5LvbBNTTiJvmEesbgUzzIuUCxpwo5DFH3HS73e5NacCihoGl5uQKOYbHXbWImzd2jF2nUQXNrm6SrilgD0L57jYB9TQcVQk/SGnu378BsMLPFXKJVvmzlYr2b+r33r9RrpBjLe1vvLNjN+zfKN/dJghVT8v9m9/YAj+3r/O7CWM8bs3/Xrx48eLFixcvXrx48eLl10scc0OLsqlBYuJR/TxzStYghMs9EzaZ69zAW/wTi2W6TfDPWWra9Xa6M5Y9xaQbBCgfCexHKloUbC0lEfzoM6L3pnIfjtk7b80P7n1R7aYiSVpK5V3SGgNv5C4p/3QpTpkh/jAM3gxTE7B/v8sZwQ9t1Yafbnmy8zkm3fI5T7umwZA5khYF7+2BGrQvsNeiiHhU/4A5pWqg2k1FkrQUy5cPtVCJN3OXyD9dilMm4UY3wb8n3tQB/r5fErbId7LsSSbdgptExTVVrw33CZ5lscd30JJPJN8z/jxzqnFq1xUp0hIZQYk3c5e0f1SKU6bGTdKJp7xVARPSiAhb2ndl2ZNMYje9VotqQ4vaw8lbEbxMSxXvRTnx88wpqoFq1xUp0pLBjUy8nbtE/lEpTpl6ntIq/x43wKshwpb2nex8jkk3iQo1qGhRSMqA43PA7FbGh6PmVSPmzzGnqAZdO1WkSEsGNzJxA3eJQimqUpwyNW6caIv/EDcdno1GhC3tO9n5HJNukzYCU/VMUXXYM/ukmBqbmEU/OE9RDbp2VRER0s7nqVu5S9I/VYpbpsENqfxr3ERF1WrClvZd4+YpJt28ximm9U2TAnle2MuAVh/lFfwwd0nXrm2RpKVz3NzMXUL/9EfALrOiQHda5QnrG0PY0r4b3DzDpJvOo9i3Ia5FJS1qL9nzAX2VIR4VMYt+kjmlatC1q4o0b8usbzDxZu6S9k+V4pZZJwU+MypPwQ0RtkzLq68izzHplk0x3DHIac+kxlCTItLbHM3C/k39o/s3qnZVkeZtgRk7Nhl4I3dJ+6dKmZVZyK0To/IU3Jj9G2p5admTTLqNNLSCL/TznKIfq+Gqf5ox9VTK1JLrTzbJixcvXrx48eLFixcvXrxs2yKoqt3zsm+V9mrYmXh+Km67F/e4cmf2+1sprviuvuRf/KgzgHe3sDIqOBn/jrpuyR6stYsX3+2f18m1soI5dSxP+T2u3Jn9/NliS1xpHgiporYj4wX/gkcR5dLbXhIF9x17sT772viMPE3vww2P5xyM/T2u3Jl94dliS1xpHvhxtny3Siqufzx+fEDNmqIeqiiGKrRhHO3zNOc6JCK5xCEmYlRFwPDYi6h5THaM0gg51DPgbxbAUqNStKZ6VuCHaRdxfXaAiIprkR+jsE4q2wgn1mMcSRqEDuuIOMzvceW+7Htll/3MbQmKLakTl+epiOWhViH/qHbt7exz9W28Sm38FEIzml5KU2hDimJIoQ1r5KzCiw+KmqhcgtNT8jCB2DFVkjc62MKd2SlKIz0r1JssKkVrqmcVvnCx/b+MG1WmMiLURjixHttQRljTYR3xTWV7jyv3ZQ/V0Gk/c1uCYktSIrv6ZleqkH9UO93N5Pt4lZRdt5JqVmteRD4hRTGk0IY1EgzTQEcxUy7VkpgVdjl0wS5luxBE3JmdojTSMxplqRTSpGf4/hkDoal813BjynSMcGI9quNbtO8Uw2+zK/dmZ+q8A/uZ2xImtuSKaZxUQoUbWTvdmTlVtaAbr5ISTesq3Mjs5Bg16/ThlKENKYohhTasFaVER01ULkk6XRLCiFvARwpPdQLO+Z3ZKUojPSOvqRTS1M+AEFCkgvJdww2VOTPCjb2mWlKHdRRRdE9L3JndWd8stoSJLXkzbogZ5OCGWtCNV0mJpnXNCtGE0KRmtccbCG1IUQwptCHlcD8muCiXEwYc/GUWpXdmN1Ea5TOyi0ohTf0Mouc5lK+LuKEyZ0Ys4UaHdVQE082u3Jndwc1iS5jYkiu+zpDKFdxM89A38SopOzlGzTqN6jK0IUUxpNCGukKKmggu1ciTz4MER2B5eNVjspsojfKZjAxb1VQKaZpnYWJFsKqBWFPH1yI/zozQuKFFaJhIk1RYR9XWW125Mzub1sXTM7clTGxJmXgdN6Qi/bNwc9ZWK+JVmsFStZJqVptwDaENdaBDFdqwxu/7UCGFRMQuS3EkTsMo59aprvdnpyiN6hk2YSJD3mMpWlM/4xYFSR0I1l2L/DgzQsd6pEUonu6rje/oW9pGV+7Mbkyyn7ktYWJLqsSrQirSP107eTujS38br1JnpxCa1FXT11YMZmiiGM4iIzpBDdulau7K3rQtV+xj/dvchp+VQs/CZN0uhClz0Qjm/OZPGh83d7lyZ/a5SYstIUyMyW+3F1eosBvjVVLwy6WYmXdGMdyaPUzzPI3W2A+xyYtHl/kT8Rz/WDjIO6MYbs3Od0VerWrovNiLR5f5Si3hxYsXL168ePHi5eUEyTlLTJ8ZNegl6VviDp7RP6mPV/Eio8xtHlelDdvVP5xafSdiFVGxqbl7QJc4PzDc1mRXt6qXmD6aGsReh751/m6quodndKXcdvHNxq31iSJNqkVmkGke6burAr+77NrLpiXWDsXquyaVP3EX+AYd+B5yHzUF6kRydmaOq/kNbpaYPpoaxF6GvnX+KvyHcBMu4+bW+uA1cMgvMcqoeaTvMxWxTyN+8e1/Wxgr1t/xXdNAHLMQXqXB0RocfjVfwf57F8HLh3q+r2FrWoxLSQ2yOE/wvkjzmhxuj6IGLctz6FvzETuyT+0gnpGbz/VIPdO1GxbXRdxo/7bWF0p1zSijlicjVPOQ76QyeXtxpIeDtiCmXXfLHb0PY7xLBZ6bs1ejUAEH53RpXJiZpjIgmWlKb4kaxGzOk2b6fM/tYc+mb9kCnlv96JSp8zke0TPSnFhcF3FD/m2tT4UxJD5USP5pI1TzkO+kYnl76ZQnKBLgl95yp96HAY8CSoejNaDOXE+5sT0XgWExW9Ck0UtRgxzOk37z7nJ7rtK8/zF9a3mLv+JTP7pl6nyOR/SMNA2L6xpucutAmZvroyMFqV1MK5HT7jwlVSxvwwvU8g7OStoZUK27ky+m4gCGwrZO00i+IG7kgFarc300iyuSyJlpmsWSpAY5nCeNG5fbw66d5PmP6VuL67IEAtmmHXdcmeVzPKJnpKlZXFdx01i4ubk+kRQWP4H8M06frW/wVbTxtrrwVjfH8FLG6vV3CvgBgAL6qaAfZHA88Q0+Coa+VdE5KLam/pjszjlPE9PnO27P0+lb9ro9hLiAue6lWZkmn+URPTOaisW1/KVFukL+ba2Pes18nnbMZmoRbqTvGjeTt5c+LEECZB8zfay/sw5ghVN5WSv/BPIQ/GWwwYFJFLQzTTN2SmqQy3kipg9xe6o8zusguP5F5jn0rfOz1Cp2oUyZz2Ur0TOjKX24QEWRZ2qRf1vrkx8vAwrjH2WQzUO+W4wr5S2/9NsDWJXhQVu02l1/FwcxfD3ai10T4wGktVy1gQ9xOH0uFGpmmqZiSQ2a8agU04e4PR1EEw7S9LvjNp9A3zr/omutU2dlynwzthI9I03iJ138oUDBtH9b62vk12ndLtTyOoNqHuW7YVxNtPr6m/0btdpdfYcrGaigpQNI92oaDNBcjY1cjTUzTWs7kl+6Udwe3gCzqG1XcHteib5lcrv5XLaSfGZqFy2/vis7+bexvhq+s7XzHLMGcYhXshQB5+WH3+4Xq43e1XdchW6Ai7s3zJdO7VrQfOzJ069D31qfb33t6/27VF9dpOHNVtc3BpNgf4X99RP0rfX51te+3r/L9Ql+s9WCMy9evHjx4sWLFy/sx+PszRk7q86Ogre5ILPNwBDTmrupQVtN2kphmrYvY3FW+zJbydEklauHbj+q6Id1zlbLJgrJnLGDryq+Z061SdR1sy24oIM3Nvt7qUGbTdpIYdKHd6n63NpnbKUlTa3ilvlTRT+qc7ZaNn0JnTN25NlR3zOn2uUXV81k2lZq0FaTNlOY6PAuVZ9bu8tWWtQkFbfMnyr6cZ2z0bIZb2Vi7ODZUZo5pRhXM7bSzDRiAWkykDLtbmrQFpO2Upj04V2TZVPtLltpUZNU3BrIsUcX/bjOuccymyRrGDt4dpQ+2UkxrmZspZlp5lfxhWXavdSgTSZtpjARBdTUZ9U+YystaZLKrAZy7MFFP7BztltmFhcWY4cRW0b+zzX5Il8If2WGQnoBoy/Kh/uoQdtM2kxhoobR9dm1a5PcY74cTVLRZbqOPbToh3bOZsucI1Q1Y0cQsYcYd6Eyrblm2sUpdDs1aJtJd1CYsGF0fW7tZJJzzJerSSq6zLljDyz6wZ2z0TJThc3Y0W+bJXvIQFqzlfjNuNlEDdpo0h0UJmwYXd+ZnbU9PJ9rksqsTOPYA4t+cOdss4wZuyzGTqMZo+rYJsW4Mmyl0C7r+6XXZmrQRpO2Upjo8C5tmVP7jK20pEkqpkzbsYcW/dDO2WwZM1FwLcaOPjuKmFOKcWXYSoF9atP3S6/N1KCtJm2kMOnDu8gyp3aXrbSoqVWc/Rty7KFFP7RzNlt2vmVrHz2l2UNIO5rYSnbo5DYJW8k7ok1EvPC2VcB9DDXoVpO2UpiWLRNX2EqOpla5tl/8iKIf2zl3WsZuYCsF9raPinvqakaY9C/ixy6b9BflBTvnjK20d1DYoMyW8ZgknmfSH5QX7BwvXrx48eLFixf21ryt1yMfeXlheWHykZcXlhcmH3l5cXlR8pGX15ZXJR95eWl5WfKRl5eWlyUfeXlleS3ykZd3kZciH3l5G3kp8pGXt9vHeQ3ykRcvzAcX9PLKURq9ePHi5S/K/5X4hvDicePF4+axchhWrEzb8bCyONJ0L+tkGC/aWJbl3OphYLeZdHEf4mtFGZ8fH4tqXfVXcTNki61a2p1YZlnWH66qWJrl/NKWWbnCkotKBygmO69npYPfGXE6rijk63RcUGsKiE0mPG4udGKZjYex71f08yErD2U2uBeuOm47bsolE8uVuPnOiM/j56pyPs5xA0GiUhMV5s/gZsxg17Yfhmzos17Q3Sjv+GHMynFsKTHDjyz+P6ImE3DbD0ZFJcJMAh1TZi10UO9exCisLutLCVZUH6ZkUfZWv6rCGMfEAUzLxpHTHY4bUtHBjZPBLWxuxH523P3xA7FzPH4hgo54J054kXcnCRh8SLgR8u74xSHp+Jnk0d+bp+RAk5UDNGqfDXQ3yu7GXkCoUCIoiGxE9KAm9EQ5lJOKSsSeyqC/4CngyLm4Q4kcPDBPZs8+vax2usPChEqUs1Q20h1coJZxhhsng1vY3IjQZW18HCEoKEDhdPz8Apx8wOhzOn7BBVEDAAGFj88Pg5sTJkLS1/EkjshBC/8ubnrG8UK4gbEDGzgrJxU5kCBupCbPemteoexMYqfsezVgORejCmArBcwbTI5xBxi0MkocqUp1R4XpRCanSLoDA8dBrZ2YKdPN4BY2M0KdOSU+TjA7wdjSqqEEXnYgIE44/HAAyif8U3dMjjl6nvrE8ej4cTrBkPR5lEGUgj+Lm4ODm5Gd40YNDqQ56o+yUZG46elz37Mscy9aFUvpOVw4dGMP+AMAUOJA1WqVnpZVFm7obpCmGNwsZXALmxmhQQCjCS6ITye9MoZRBi8IHxTADVco+rJw86ESP3Cu+jipn+xXfxA3pQLMoC4lguIww02JM8AwjuWBNOfjjUw0PZcdBM4S9mW25IURA/UJN2blzazVji5M1m5wQwYOC+tiJ4Nb2KIRHy37PMGQ84UjDaHhhLj5PH7RQplw4443OKt94Sx1ZHIpxKJp2vs762K5mIQ5A5uW7ibcQK+ORmVkI/aF1MTlw4i7J1LFJPb0jVl9kbIvkBdyTF/aYRjgM9wcJMhmuBl1Is1T8k7gIrkcJNRGtpjBLWxuhL0uFrLvASIn/nH8+jpyAIQARHD+8aVxc8IV86cEF8d56kuORUdMjyPgnP258UaoxSQO/P2o7wxuMLmkRNoCIU2GQ79R0Yn0NX1p/4bLqaW3BjrEAiBgwg1qlnPcqNVuz6Y5EO/kMnmY7eC4GdzCZkbY62L93VpOW0xOUJ/q29XR4Aa/XSG8UAdnM1gVnzD96/gJJ3YnUfj39v0OauuBc/vOwOrQTokCt1xhSuL6mdAqUyLtvqrNWfeyxhZ+2cDZ3aG97tHhcOCjNSnNjLDOnPrSWzfiExJhvFmO9fh5OdajFY7Ov5+6ZYfwwrbhM6WU3/Jv5/V8HZl/P/UTw1MpViY+V9qh7MsNaP48edx4Yf59uBcvXrx48eLFixcvXrx48eLFi5e/Jv8Brw6NUBoNPmYAAAAASUVORK5CYII=\" width=\"567\" height=\"163\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--write-the-caddyfile\">Step 2 — Write the Caddyfile<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#step-2--write-the-caddyfile\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Write the Caddyfile\" title=\"Direct link to Step 2 — Write the Caddyfile\" translate=\"no\">​</a></h2>\n<p>Caddy's config: serve HTTPS and proxy everything to the gateway. <code>tls internal</code>\nuses Caddy's own local CA — no public domain needed.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> Caddyfile </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"># Caddy selects its certificate by SNI (the hostname in the TLS handshake).</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"># A bare IP sends no SNI, so we use a hostname. With a public domain, put your</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"># domain here and drop `tls internal` for automatic Let's Encrypt.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">openclaw.local {</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    tls internal</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    reverse_proxy openclaw-gateway:18789</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">}</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div></code></pre></div></div>\n<p>The proxy target is <code>openclaw-gateway:18789</code> — Caddy reaches the gateway by its\n<strong>Compose service name</strong> over the shared Docker network.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--add-caddy-to-compose\">Step 3 — Add Caddy to Compose<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#step-3--add-caddy-to-compose\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — Add Caddy to Compose\" title=\"Direct link to Step 3 — Add Caddy to Compose\" translate=\"no\">​</a></h2>\n<p><code>docker compose</code> auto-merges a <code>docker-compose.override.yml</code>, so we add a <code>caddy</code>\nservice without touching OpenClaw's official compose file:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> docker-compose.override.yml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">services:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  caddy:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    image: caddy:2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    restart: unless-stopped</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    ports:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - \"80:80\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - \"443:443\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    volumes:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - ./Caddyfile:/etc/caddy/Caddyfile:ro</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - caddy_data:/data</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - caddy_config:/config</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">volumes:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  caddy_data:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  caddy_config:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><br></div></code></pre></div></div>\n<p><code>caddy_data</code> persists the local CA + certs across restarts.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--start-caddy\">Step 4 — Start Caddy<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#step-4--start-caddy\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Start Caddy\" title=\"Direct link to Step 4 — Start Caddy\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><br></div></code></pre></div></div>\n<p>Caddy comes up alongside the gateway with <code>443</code> published, and generates its\ninternal CA + a certificate on first run.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Caddy container up with 443 published\" src=\"https://development-wec.wiline.com/docs/assets/images/caddy-step4-up-d53a3b63c4e346428a8e733249b2049a.png\" width=\"654\" height=\"359\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>note</div><div class=\"admonitionContent_BuS1\"><p><code>docker compose up -d</code> also starts the one-shot <code>openclaw-cli</code> helper; it idling\nhere is harmless — we invoke it with <code>docker compose run --rm</code> when needed.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--verify-https\">Step 5 — Verify HTTPS<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#step-5--verify-https\" class=\"hash-link\" aria-label=\"Direct link to Step 5 — Verify HTTPS\" title=\"Direct link to Step 5 — Verify HTTPS\" translate=\"no\">​</a></h2>\n<p>Hit the gateway's health endpoint <strong>through Caddy</strong>. <code>--resolve</code> makes curl send\nthe hostname (SNI) while pointing at the box:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-k</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--resolve</span><span class=\"token plain\"> openclaw.local:443:127.0.0.1 https://openclaw.local/healthz</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># {\"ok\":true,\"status\":\"live\"}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"healthz returning over HTTPS via Caddy\" src=\"data:image/png;base64,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\" width=\"882\" height=\"41\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Hit a <code>tlsv1 alert internal error</code>?</div><div class=\"admonitionContent_BuS1\"><p>That happens if you used a <strong>bare IP</strong> as the site address — see\n<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#1-tlsv1-alert-internal-error\" class=\"\">Troubleshooting #1</a>. Use a hostname.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-6--re-secure-the-gateway\">Step 6 — Re-secure the gateway<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#step-6--re-secure-the-gateway\" class=\"hash-link\" aria-label=\"Direct link to Step 6 — Re-secure the gateway\" title=\"Direct link to Step 6 — Re-secure the gateway\" translate=\"no\">​</a></h2>\n<p>Now traffic arrives over <code>https://openclaw.local</code>. Two config changes:</p>\n<ol>\n<li class=\"\">Add the HTTPS <strong>origin</strong> to <code>allowedOrigins</code> (the Control UI rejects unknown origins).</li>\n<li class=\"\">Turn <strong><code>allowInsecureAuth</code> back off</strong> — HTTPS is a secure context, so the\nArticle 1 workaround is no longer needed.</li>\n</ol>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> --no-deps </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--entrypoint</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">node</span><span class=\"token plain\"> openclaw-gateway </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  dist/index.js config </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> --batch-json </span><span class=\"token string\" style=\"color:#e3116c\">'[{\"path\":\"gateway.controlUi.allowedOrigins\",\"value\":[\"https://openclaw.local\"]},{\"path\":\"gateway.controlUi.allowInsecureAuth\",\"value\":false}]'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose restart openclaw-gateway</span><br></div></code></pre></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-7--pair-your-browser\">Step 7 — Pair your browser<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#step-7--pair-your-browser\" class=\"hash-link\" aria-label=\"Direct link to Step 7 — Pair your browser\" title=\"Direct link to Step 7 — Pair your browser\" translate=\"no\">​</a></h2>\n<p>Open <strong><code>https://openclaw.local/</code></strong> in your browser (on a private box, first map\n<code>10.80.4.212 openclaw.local</code> in your machine's hosts file — <code>/etc/hosts</code> on\nmacOS/Linux, or <code>C:\\Windows\\System32\\drivers\\etc\\hosts</code> edited as Administrator\non Windows).</p>\n<p><strong>You'll hit a certificate warning first</strong> — <em>\"Your connection is not private\"</em>\n(<code>NET::ERR_CERT_AUTHORITY_INVALID</code>). This is expected with <code>tls internal</code>: the\ncertificate is signed by Caddy's <strong>local CA</strong>, which your operating system doesn't\ntrust. Click <strong>Advanced → Proceed to openclaw.local (unsafe)</strong> to continue. (With a\nreal domain + Let's Encrypt, the certificate is publicly trusted and this warning\nnever appears — see <a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#going-to-production\" class=\"\">Going to production</a>.)</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Browser cert warning — click Advanced, then Proceed\" src=\"https://development-wec.wiline.com/docs/assets/images/caddy-step7-cert-warning-f6015d4f4498bdd7f495c01e08e91cf5.png\" width=\"920\" height=\"744\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Now the page loads — but with <code>allowInsecureAuth</code> off, the gateway requires\n<strong>device pairing</strong> before this browser can use the Control UI. That's the secure\nflow doing its job. The screen shows a request id; approve it on the host:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli devices approve </span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">request-id-from-the-screen</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Device pairing required screen\" src=\"https://development-wec.wiline.com/docs/assets/images/caddy-step7-pairing-95202988ec2160fdef5dcd2e33fce67f.png\" width=\"1100\" height=\"508\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Back in the browser, click <strong>Connect</strong> again — it pairs and loads the dashboard.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-8--lock-down-direct-access-the-real-hardening\">Step 8 — Lock down direct access (the real hardening)<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#step-8--lock-down-direct-access-the-real-hardening\" class=\"hash-link\" aria-label=\"Direct link to Step 8 — Lock down direct access (the real hardening)\" title=\"Direct link to Step 8 — Lock down direct access (the real hardening)\" translate=\"no\">​</a></h2>\n<p>The gateway still publishes <code>18789</code> to the host, so <code>http://&lt;vm-ip&gt;:18789</code> is\n<em>still reachable in plain HTTP</em>, bypassing Caddy. Close it: stop publishing the\ngateway's ports so Caddy on <code>:443</code> is the only door. Caddy still reaches it over\nthe internal network.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">cat</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\"> docker-compose.override.yml </span><span class=\"token operator\" style=\"color:#393A34\">&lt;&lt;</span><span class=\"token string\" style=\"color:#e3116c\">'EOF'</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">services:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  openclaw-gateway:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    # Unpublish the gateway's host ports — Caddy reaches it internally, so the</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    # only entry point is HTTPS on :443.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    ports: !reset []</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  caddy:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    image: caddy:2</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    restart: unless-stopped</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    ports:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - \"80:80\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - \"443:443\"</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">    volumes:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - ./Caddyfile:/etc/caddy/Caddyfile:ro</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - caddy_data:/data</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">      - caddy_config:/config</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">volumes:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  caddy_data:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">  caddy_config:</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token string\" style=\"color:#e3116c\">EOF</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><br></div></code></pre></div></div>\n<p><code>ports: !reset []</code> clears the ports the base compose published.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-9--prove-its-locked\">Step 9 — Prove it's locked<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#step-9--prove-its-locked\" class=\"hash-link\" aria-label=\"Direct link to Step 9 ��— Prove it's locked\" title=\"Direct link to Step 9 — Prove it's locked\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-s</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-m</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">5</span><span class=\"token plain\"> http://</span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">vm-ip</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><span class=\"token plain\">:18789/healthz </span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\" &lt;- direct HTTP (should FAIL)\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-sk</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--resolve</span><span class=\"token plain\"> openclaw.local:443:127.0.0.1 https://openclaw.local/healthz </span><span class=\"token punctuation\" style=\"color:#393A34\">;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">echo</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\" &lt;- via Caddy (should work)\"</span><br></div></code></pre></div></div>\n<p>The first returns nothing (connection refused) — the insecure door is gone. The\nsecond still returns <code>{\"ok\":true,\"status\":\"live\"}</code>.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Before/after: direct port refused, HTTPS works\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABEEAAABrCAMAAACvx2ylAAAAXVBMVEURIEBGTmV+g5BUXHCU3FVobn+ipa3d3d0ZJ0XS0tSIjJiRlZ+7vMF1eomwsrgvPlCBwFA6RFzFxsmO0lMmMk9eoelSeUdzq05llktcY3ZFd7IuUYAPh8w7WERSjc4yiTzVAAAACXBIWXMAAAsTAAALEwEAmpwYAAAeV0lEQVR42u1dDduiLBPV/ADL8jOzeuv//8x3BgYEsrLu3X3abc517XpDfIwIRxjxGEUMBoPBYDAYDAaD8bfiMsrniarhsrA4SukfIrkk6zjctfFQH/yYYRzH6DWT7hnRnxdYdz6dTnPxu+xxvibLsrcui0RYE7NFDRj1R2kPOkcfV26COJa3kV7DNIlKQnEm5CdUpUhjoS6zSXprRDVnrt8SS8/oSYY4k1myuIwshhzrhRVL9U96iKS8m/Zpu1BSNOL+GbmtZFPqQm1FVIH8FaP/af/M4mpVPW7rMa3momt3ONdpmnaXh0kIB0hZh4eqxv+f4m6iAYpJb+tZ2EjPjLjuFxRy3u9nkjW5KMqHV3JbiOKdK9sLhLm4iaDOtI7dww3yQtKhz8UGq4dC8mm8tULsZBCJiWwn6iGFKPtIZzch86MuU5cSKwMLCskcAitjxFHED1pCGZ+I+LUGmc+wFnGxW1xGscEczd3f3WbdCjjvIi+EPk99wJNuN+EFX9guavDnMxf1Xn9BczXwOmAxmVhDp6OuUeSvdqqZbvO0fxabuBC7/lFb32GQ9OCOwWEYuk4+SGLHen2p09E/VHoIv88g9ZyJ9UIGeWbEeX9eVM7plkFkURb56sm9YP0mg2yOx2MVdjbqjW6ndO8X4kgHWRTYq2ORV/nED2XZrEQSREIisbVntG36tcgoO4Xs2apIKqWHm1cmSgqtRBbvtJFQ+x16oJZQxv8iBslE3+YvMYh8UK/brFtRIYPEyVZsEoA6NGKdrMUm7AWL2oUIZNs/PiOnvzgMUsZxDBlz0eIZAJFA6HUGme0262cMEslj0fZzbT100EbdOKZjl9aSQkMHoU5eYNwNQ0WRKc5BhvTQDZgykhDsRptER9Y13OsPMNYl/Ogf5CCdwdvhn2NX1ykWbNu27pwRrguDaQNEjhKmIFARhXAuoajLYxAvgy7sMNGPZ8Sx8Ntxf9IzjBNyCR4iebWhq6IOFasZRKrQ/twD7+zPIi+dOaBct0WZJXjThujNZlViVXNXKMmLGzrPN2WZ2U7Ti610h0+2K3LZJCJPkp4OSZlg5HRGve5TeJCJbKGojehlK2xNuyLORRNEyradGITu65SdQlSDidSl4G+bIqbQroREahxh7WQu3JyLFjL267LFdlItYYzXSeIScZwYoXBXOjo7NSuVmZRFuXVmVVG+nV25hoNFrspWbLA+bFe6OF4NZJnPIEiwmebZDG//8F9R2kJfaBeFlW5pffn9MzLdhvqLNtcySE5cJRTraJMeMIjpGtRY+mz986OTNvVRSuqYpl2METHOWW/bekwvOI8YYXh16UChQQ38A1IGjEWKhAQSGKTWKWFM1mM9JdGRSAApjFz4FSjIO/jTi1qVCXlSd20C6Q5TIl2Y7DDyoNYw6UAhONRYn88gXga/sNCIrTeDB2KAnr7fn6/7MzLGCZlhfz6Z0AkSnM6ns2GQ6/563mPU/ir3cE9waT0XebbZZtj1IHonio2aNs8wSNtm203oUREwV20LZxVTJhODiBJuXxs9f6VDBpE7nHWYM9oUlXPAXp3DRYd5kjPbgC4cROZF7DFIpjsuMUhGiyFdQ7uxpajfMltm0cPp76h2MldCt81FInditxKGS8l4SlJttyshEoe91tOY19mpWSlDI9pVKZ65lmCOFbY6WLcqNv02F1I1Nl4cv4aNt3DcivVqNccgsTMHeaFd9DVuncvvn5HpNtRftLlOf8nAa3IUahnznEGoa5jG0mfrnx+dtOF1Sqktm9rFGJEX88sXYpAuqoATDIPAYgWHmrqHGwZJ4RaPDKJSVmZwUhIVCQN1jLpD1w3pCOm8gxm84wEgL/D3gPOeC1ZKkYOqsqYQFTYYO7pOr0bIwEH9qRlkLoNfWGAEtBZ6oU5XJAXoa3tFDsDMihpwStIDP5wVR6gJyv7krGIoyfUK05TzXi0j19PCI9dXzzAIXLpwDpJtATIqCneGoSN3RQXXVFiH23ZT2Glugu4LrMldxWRQn1R1NlL38a1zUL16V0Knm64+9FkoxI+E7i5dBjmKXE4MYkK6Bh2pS4E41YV1KBEFrvupdjI3Frv4CPN+Y7i/iqEzgm7q+gkcxy1lp2alDDglkuKx6wO9U9C+1NamsI1eFmyJQfDi+DWEqxiYrogbBtnB3dlZUi9uF5pD6Ir05ffPyGcQa67xg7QtVAv+rbadZ5C+UrAXVXUN01imK7rnZ09a1WdSasuoXRwjtrPeI8sgl0g6DDJEtwyiJwyUcjALBJsE1wdqlKPLtYvS1D+YwVt3HTpTurTqUtnVEic+FDlStSZJR+4Xh0EoNCpTLIPMZfALC4wwdAAzDHShXq/Gl6opQREJAhikJz5xGERzzemEK5kTZi0nJ0KsL5BlkN3MOjMv1TQ6aUUx+U905G4jRZIVt7MBs2QubhikIQah1Yian9OB5iAC5qRtaztNWUFn8iMLGCngDu5tTbk3KHLPy4ORVEqk7q82FK+BKXKqncyFO2FRFJsVhm4ZhM7oxq3g3kgxOzUrZVBTiLJ96HiCoSuntjaEkHgMsrutYdYPEjJIm2/7mTnls3ahS0Tnqi+/f0Y+g1hz3VUMuN1gZRTPMgisPhC26VTXMI1luqJ7fvakVX0mpbaM2sUxIpv12eDYP+B4q+DPA4UmBqmnJF09DsPhQill2jmeT4qcGOQicR3kHgInKcwiMD0xiPXVRo5XxBSmaqdgbQwcZzypXga/sFkjTlWE6xaYU1S0klHUoSYZ5FolBomUL2RiEKmT7CPlHInKrdM/cn3bbqAD3GGQaaW6C6fiyCCxnYOoufgMg+gZjzpgn8um6cNKX+SVudatevBQVBPJ4Oy6wjuTG7ldrdYwWZXWNyyn7DJ80ISRVAr8W09laiNXVDuZS2NvDXauXAZZO2e0DThKun08c5qVMmzwZvn48YtcgWczdL2jYYqRXQbxa7Ct+8QPEs0zyMN2sa5s5/L7Z2S6jW4la67LIEdBj2QWrGJU1zCNZRnEOT/pzUGmZkXL6JQdIzZCzj+kQKfkiHP8tKLQxCAwvgebZIgGHJUqJUwiugH3bqgkNlKN4QsMV3jw4R/koJYertNCBgxyUXQTMIiuvZ5WMRiS6FatR2QGt0g3g19YaITrSZV6ggELll75NnqgBgnc0Pe4kNEMAkQjz3qpY9hDMch1f4pL6KzeyjLJ4EHEZi1cBoGnHzfT7CM46h8ySLOOwYmfhAxStjE+k1cH6KSrAn0e6owafanpIJOk2CVxI0pwnGY2SR6vcOHtRqrOtDVeQbhfwoo7puwUsh5DKlOVAkmPkS1TZk3cFj3VTubCQ4rkuIH+mMMSfGNbQhlvSUboGuy4XbnPODC7bVbKsINJ1PbJVo5VEc5sYP2xFppB0Ah9cfwabOsuZ5Cl7WLSbxTB0uX3z8h0G91K1lyXQWARI2XRSvVcCJ7FlNB01R0GUV3DNJbpiu752ZNW9VFKsozaZTKin+dsiQ8s9LKgG0xoUOuWGucWqXJbpto1iSOSUqqkNomJ1GN4dj9IpRYenTP5QVbwGARrqEMGwQXIwTKICSnH6qj3qETzGfzCAiNcT6p5QqsWNeqJDM4/9NEwCD6Z0c9icEcIPqaBpPpZTbUrRLv1HvbDBBr8VivoWfnO3P2ECB19sPYp8oDVd2tkkMJxek4DJcFlKDLIsVC9TB1grgnrVePP2+mtIHTQ20l2auvH5AoFzwre8v1IZJDMhBKVb03ZKUS/mTKpFNr0oUP4m3oAIV1zG/ABlJlEtwSOAWoJZTwlyUwNdqrgLFB0dmpWU6Yy/ulmKrlqbzypcFXUqcCjVnNxvBps67r7QSxrqEMVMMjSdrEXHsa/vfz+GZluQ61kzHUZRE06NiI2+0E8x2i4AFRdgxrLdEX3/OxJ6/p0StMxqV2sEbm4s23voq9EJd2QreFSTZES796wpjGTXf0HJpkizQbQ6vawZOOovG9gELrcL1MluVSVHJzNKr4RrrPubLaCSNyXesZZho731rrS3bQa2Jl7l1Aqz1r1fLegbJ6lkXEsZzP25gCLXTmdUaxdXXHg8aLtp3TSMu5vI2fa5dYYP2V/E+pjxwintfqZNqNzmEW5u83ut5as3tuMOZ/Nr+GRZT9pF+exfLvqw8tPZ+QbWP1ky+nUNfxi/POba1YT51w4mZV39h+9jNm9Z3c2pP2XOKhVzOtX4LyP/iYo39s/hrVIon8asOFi9Zd1DVzL/qKihloujPxvUY2wweyNJjxf/6rO2KzlPzfAjnH0r0PKv6xrSBkxGAwGg8FgMBgMBoPBYDCi1xRHsub3qJi8n/213+R0kHFS3f6kBXWkp+4T6PEE6jzybdEcVw3oTzTB5wr/qByzvj/pXySjuZPFL6kBxXH/c42f++PhqQLPd+Gh4sibj4XfVdlZkP2l30hJZ6s2HHvKL56gjtGIsblcPR4/RNoy0UsyQFGoBvRnmuBzhX90jtu7k9nzFEWBYkYRbDh9qAaU4VWVP9X4eTAenirwfBvWv5xB3lXZWZR9+W+kpKMOsNu42Yne14ghQR2jEWNyuXo8fshoy7wkA3SjBvRnmuCDhX/0/qsZjoW3//03Au4yyH01IHjbo6ni6KcaP4/Gw10Fnn8eLyviQIs15R2a9TVbKIORg1Fl7rB5s7KnJFYMReurJI4mTVweVcgXQdFCNeWxKY+ybPzLTRVRBk/Sxrz/QUo6VlCnV1uwPY0YI6ijNWLMqwyuHo8fcvI9kwEymkIzakB/ogk+VfiH+kRCb9EakyLnFR8TSXI3+pBTX3quBtTjO/RR9EONH9PknmTPNB7uKPD883hZEUes4qK44wvxNVt0BisHQ+//NajXZpKYbkL6KkoShX4j7RVfBIWEaoo1vIzQTHvuPIEUncGXtDHvf5CSDh2SUkxvg4SCOlojhhrJ0+MJ1HlsvqcyQEZTaEYN6E80wacK/1CfIOEfaxJdC2WAiSS5G31QC1L7putDNSBQBnS8E29q/FCTx55kjzMe8iL6QryuiCN2RduEv9HB02yhDFYOhvr4Bl47Opok0z1f6asoSRSrSaO0V3wRFBKq2YHqQeHIaLgCKVaMxpe0UTowpKRjBHVi0GVQg9jViDGCOo3z4rSvxxNI9gQM8kAGyGoK3aoB/Ykm+FThH+oT9NJ94rSe1SmgSJK7oQPoDhi6eKoG5L6o+67GDzV5INkzjYetaL6QQV5XxPHfBaTf6OBptlAGKwdDrF70u9bIl9h5EOmrKEkU+s2+9+yKoJBQzaYtNmJX+sOHSqEMoaSNq6TjCOpkrpfOE9TJp/exA5EePxQyyAMZIF9TyFMD+gNN8LHCP9QniEGsSaSWljmnQnI3RvVmK46TD/qJGlA1zY7e1fihJg8ke6bxkIn4CxnkdUUcsSmKey3labZQBqtbosuE1SLcCmNvsktJSBKFfjP6EoEQmxKqgYchVeE8DnEFUihDKGnjKuk4gjqNO0hcQZ3KpRZfj8cP3WOQWRkgV1PIUwP6A03wscI/1CeIQaxJ7hyZIknuxqjeoBN7vVQNqJ8a602NH2ryQLJnGg/zCjzf4Ad5TRFHrJq7FOJptlAGKwdjtGUE3DaMrIudE2MSkkSh30w/DkRQlFBNDE9JVJnkk3QFUihDKGnjK+moQwwCPjnOnl2NGCOok9DzEZLl8UV63BDlWyADZDWFZtSAfn8TfK7wD/UJEv6xJpm7y7rJEookuRuregN+mmoJgzSbY7WdHni9qfFDTR5I9tjx0H/pY9yXFXGgecF1NM+2nmaLyWDkYKhMzekkX+JssNhII4mifzPaK54ICgnVwIdUtI+NfJKuQIo5o0DSxlfSUYfE7AdxNWKMoI7ZN6Hz+Xo8XshoyzyXAbKaQjNqQL+/CT5Y+Mfc55XwjzXJ658USXI3RvWm9zbp3FcDUrt7pscxb2r8mCb3JXvseMjFt24l+4WKOIFmi5FsmS276kN9FSuJ4v/m1xuopcQzKi2zZzSnndPTRsUFujq+Hs+sOs8SGSBqkDk1oN/dBJ8s/PNQBkiqxq48ISw6rJZ6HkCqqfp1Gj8zp/kLFXgYEavlvH1in9wEHyf8k+diE32Ixs8vVOBhRKyW8/aJfXITfJzwT54f5adI5rACD4PBYDAYDAaDwWAwGAwGI7IyK/FSN99jrZdZwZ/YU/XR39dokj76uSqMEQqSJiC93ZXR79Fh0kYY3RkG49uhXth98OaQq6TzUOvFB6nzxPhtc2lUfXRIbSVaRT9VhWlp25h6AaYxX/p6WcJhRinoqWYSvDdCujMMBjPIvABMFN394vms1kswASF1nrKE/cWJUfXRIdwQv5uY4l1VmLaM1b6xHHdUSvgm7RpCrzPI7Hah9TMGMbozDMYXIRSAIZmVmN6m9FWEtMQMSdNEz7VeouB7xVqdZ1fACyqNUfXRIXzv9DhRzpuqMPS1UeAqoVjn6RfPrRqQKyPjn1+g+EMpST7ItIsxIhY5dynGN/HHjQAMyayQAEygIqQlZkiaZonWi/e9YvNea2y+MK1UfXQI367PJ7WJN1VhMEOW4aeQ9Xv8TxmENH4aT0bGPz9f8cek1JZN7WKM+E7dGcaXYk4AxsqsbIlBHBUh+2qTv4p5pPViq5KuJod+hUur+uhQIop2emPtXVUY8IO0LVQLOhJK0WOGQfpKwTKI0vgJZGS88+uDt+11Sm0ZtYtjxHfqzjC+E7MCMFZmhRhkdysxM+sHuaf14gMZpBdlpYQzlKqPCcVreOkz/6EqDK1iQPUDVkbxLIPA6gMxffEcGSSQkfHOz1f8MSm1ZdQujhHfqTvD+E7MCsBYmRWXQXyJGSuos0jr5YZBMvXphZxUfUxIzUZWP1SFIQY5Cnoks2AVozR+AhkZ7/x8xZ9JVwctiydNLTLiS3VnGN/MIcHKxsisKAEYX0XIit+QoM5yBnHVeRpQBkVPqVb1oRB8iyFuJ6XBN1VhiEFgESMlqEKo50LwLKYE50h1h0GUxk8gI+OdX6j4o1KSZUZdyRrR8+dDGN/GIe2NJ1XLrCgBGF9FyIrfkKDOU60X35Nq1Hk2kDuXRtVHh/A3RwvtTVUYYhA16diI2OwHEb7aq+9JVRo/voyMd36+4g+lNPJB1C7WiO/VnWEwHsqsBBIzj6RpHij32FB/E+pjL8kPVWGixQodRg0oEPLxzm9OV8fEOco9rDvDYHwMfrkqzO/X+GHdGQbjkxZY8i+TOWLdGQaDwWAwGAwGg8FgMBgMBuOfQbOCnWFxJqcdU1FG3zLxIhfghxl0yCr3zCka6STxZv3qLot5eSSZVDdCSpEnULREMymO+58rIc1BXwfWKWJ8MuC993KF74c5mydpE4MfOaeyE4Xv1TzIMJfdz6BDVrlnTtFIJznCx7NfHFVzhZmdYVEU6IoUwbbch5pJGYoYyZ8qIUX3VUlYp4jx0VMQ9V59JvrpS4OGQfzI6PnmqIcZ5rL7GUyIlHvmFI1Mkli8uNFirjDYjF753we+yyD3NZPgnZimiqOfKiE90jVinSLGR89BtM5glG+tWg703Aa+cUiR9KVoUtnRmjtG1YcEdcx0nkpRYjsmQ78uWytCZPKRHo9Xgw3pt/T168E75Ius7KkikyS78858KI9E2RN611jbYrlTj1+KJFEgfcip2ueaSb3zQdbobSWk8tiUR1k2nrCRuQ6sU8T4ZDgvqhq1HLGCjwk3/jssRmVHa+7QG/VGUMeHFtuhDPDWym4lVhQyb+LrJDd6Q849mhSNNkgVbRlWtJpdxdzKI1F2KoxssfMZVZ6JJFEgfdgiPdj3gR9qJoF+otMAbyohFWtoggZWRq6wkXMdWKeI8aFQHdbxMii1HLErWvcer9/40ENPa+7QaCBBnZCTiu2UIcGfhRHpMaPIJAn0hrxZvlITwInCURzDiuKivXGNzskjUXYqzNrifreZIkkUiA4gKOKslB5rJrmvM7+rhLQDRZIiKwJho+k6sE4R40OxE85a3ajlzL+5SgyyiyYmIEGdIKEW26EMapzeMIhJEj3wE2g9ElAd2rUyrKgH/2+/QB6JslNh1pZIi4ZYLQ+IJFEgow20FUeQBFimmVRNLPyuEtKmLTZiVwbCRtN1YJ0ixqeid1YxRi1HbIritsfqKYDu3KTqM92mfTZIlOSYyrCGcaNWDipk1YAoyXMGARcAuBbiG3GAeJE8EmWnwqwtrlohRZIokNEGQhn59VLNpH5ahL2phAQSAVUh8kDYaLoOrFPE+Bv8IEZXR6wal0K0n5NUdnTnJlUfIzRErGBWE0pshzLEOC5wpKgQ5bNJ/BrsbEI9IdGKRlEpYC5gK3Kfry6QR6LsVJi1xRDmurEGkiiQ1QYC8Y9qCYM0m2O1VdJIP1FCikF6BZIEwkb2OrBOEeNz5yC+xKnS1YEocOHJwM+pVXa05o5R9SFBHRhvky6REdvRGSR6J/AJrA7pfDbJrCdVK/eQohHdqE1F9jlpskweydznVWHWFnc/iIkkUSCjDeTMKx5qJinVoulxzJtKSD1YiZ5dX9jIXgfWKWJ8LECS1HFjyjl5HqMG5KnsmIQkNFTuZgR4dAaT0gsFSkHx80l61TtWrqa7/k/EkmRcTZH+YbXU8yDjuPp1SkgzF4B1ihgfDbgR579A5f1P3iThTt3+Xr9AnotN9CFKSKxTxPjwdUzz8w2Pxz/6qEA21W92LOb5UX6KsBDrFDEYDAaDwXiG/2lwQzAYDGYQBoPBDMJg/G5cxgXeumq4LCyOUvqHaJFDcBzu2nioD37MMI5j9JpJ94zozwusO59Op7n4XcYMwvhujOmsYE/tDuc6TdPu8jAJ4QAp6/BQ1fj/U9xNNEAx6W09C0/wmRHX/YJCzvv9TDJ8oc++h8MMwmAGcYbzwR2DwzB0nXyQxI71+lKno3+o9BB+n0HqORPrhQzyzIjz/ryonNN+Rl+lLPIVMwjjOzF0MC67cUzHLq0lhYYOQp28wLgbhooiU5yDDOmhGzBlJCHYjTaJjqxruNcfYKxL+NE/yEE6g7fDP8eurlMs2A7GunNGuC4Mpg0QOUqYgkBFFMK5hKIuj0G8DLqww0Q/nhHHYNPX/qRnGCfkEjxE8mpDV0UdKlYziFSh/bkH3tmfRV7yKobxvZOPC84jRhheXTpQaFAD/4CUAWORIiGBBAapdUoYk/VYT0l0JBJACiMXfgUK8g7+9KJWZUKe1F2bQLrDlEgXJjuMPKg1TDpQCA411ucziJfBLyw0wn8PB4hBRv1+f77uz8gYJ2SG/flkQidIcDqfzoZBrvvreY9R+6vc457eFTMI49sZpIsq4ATDILBYwaGm7uGGQVK4xSODqJSVGZyUREXCQB2j7tB1QzpCOu9gBu94AMgL/D3gvOeClVLkoKqsKUSFDcaOrtOrETJwUH9qBpnL4BcWGKEVjeTpiqQA3pG9IgfYmauoAackPfDDWXGEmqBgpF3FUJLrFaYp572STl4zgzC+nEEukXQYZIhuGURPGCjlYBYINgmuD9QoR5drF6WpfzCDt+46dKZ0adWlsqslTnwocqRqTZKO3C8Og1BoVKZYBpnL4BcWGGHoAGYY6EK9Xo0vVVOCIhIEMEhPfOIwiOaa0wlXMqerEhDQ4qjMIIyvZJADjrcK/jxQaGKQekrS1eMwHC6UUqad4/mkyIlBLhLXQe4hcJLCLALTE4NExlcbOV4RU5iqnYK1MXCc8aR6GfzCZo04VRGuW2BOUdFKRlGHmmSQa5UYJFK+kIlBpE6yj5RzJCqnRREzCOPLPKlpjU7JEef4aUWhiUFgfA82yRANOCpVSphEdAPu3VBJbKQawxcYrvDgwz/IQS09XKeFDBjkougmYBBdez2tYjAk0a1aj8gMbpFuBr+w0AjXkyr1BAMWLL3ybfRADRK4oe9xIaMZBIhGnvVSx7CHYpDr/hSXoGtkxZKYQRjfBYkPLPSyoBtMaFDrlhrnFqlyW6baNYkjklKqpDaJidRjeHY/SKUWHp0z+UFW8BgEa6hDBsEFyMEyiAkpx+qo96hE8xn8wgIjXE+qeUKrFjXqiQzOP/TRMAg+mdHPYnBHCD6mgaT6WU21K0S7ZT8I41tx8XR1LoECz6WaIiXevWFNYwSD9B+YZIqMaANodXuIXtb/iWZMMqFL9fiMLlUlB2ezim+Eq2h0NltBJO5LPeMsQ8f7n7J0N60Gdub8NJfB+Mneszsb0v5LHNQq5nVljfM+4vdiGIzf5zip5cLI/xbVCBvM3uC185UZhMFgRPxuLoPBYDAYDAaDwWAwGAwGg8FgMBgMBoPBYDAYDAaDwWAwGAwGg8FgMBgMBoPBYDAYDAaDwWAwGAwGg8FgMBgMBoPBYDAYDAaDwWAwGAwGg8FgMBiMX4z/Ayzaau/aJV5wAAAAAElFTkSuQmCC\" width=\"1089\" height=\"107\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Your OpenClaw is now reachable <strong>only</strong> over HTTPS, device-paired.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"OpenClaw Control UI over HTTPS\" src=\"https://development-wec.wiline.com/docs/assets/images/caddy-control-ui-https-814c25fc8dd38568d2226856190421ba.png\" width=\"1100\" height=\"663\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can now <strong>put real HTTPS and auth in front of any self-hosted service</strong> — and prove the\nback door is closed.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting-real-errors\">Troubleshooting (real errors)<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#troubleshooting-real-errors\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting (real errors)\" title=\"Direct link to Troubleshooting (real errors)\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"1-tlsv1-alert-internal-error\">1. <code>tlsv1 alert internal error</code><a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#1-tlsv1-alert-internal-error\" class=\"hash-link\" aria-label=\"Direct link to 1-tlsv1-alert-internal-error\" title=\"Direct link to 1-tlsv1-alert-internal-error\" translate=\"no\">​</a></h3>\n<p>Hit when the Caddyfile site address was a bare IP (<code>https://10.80.4.212</code>):</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">curl: (35) error:0A000438:SSL routines::tlsv1 alert internal error</span><br></div></code></pre></div></div>\n<p>Caddy's logs showed the cert was obtained fine. <strong>Cause:</strong> TLS picks a certificate\nby <strong>SNI</strong> (the hostname in the handshake), but connections to a bare IP send no\nSNI — so Caddy can't match a cert and aborts. <strong>Fix:</strong> use a hostname site address\n(Step 2). A reader with a real domain never hits this — the domain <em>is</em> the SNI.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"2-device-pairing-required\">2. Device pairing required<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#2-device-pairing-required\" class=\"hash-link\" aria-label=\"Direct link to 2. Device pairing required\" title=\"Direct link to 2. Device pairing required\" translate=\"no\">​</a></h3>\n<p>On the first HTTPS connect after turning off <code>allowInsecureAuth</code>:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Device pairing required — This browser needs one-time approval from the Gateway</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">host before it can use the Control UI.</span><br></div></code></pre></div></div>\n<p><strong>Cause:</strong> this is the <em>correct</em> secure flow — with insecure auth off, each new\nbrowser must be approved. <strong>Fix:</strong> <code>docker compose run --rm openclaw-cli devices approve &lt;request-id&gt;</code>. (The CLI may log <code>scope upgrade pending approval … using local fallback</code> but still completes the approval.)</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"3-neterr_cert_authority_invalid\">3. <code>NET::ERR_CERT_AUTHORITY_INVALID</code><a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#3-neterr_cert_authority_invalid\" class=\"hash-link\" aria-label=\"Direct link to 3-neterr_cert_authority_invalid\" title=\"Direct link to 3-neterr_cert_authority_invalid\" translate=\"no\">​</a></h3>\n<p>Expected with <code>tls internal</code> — the cert is signed by Caddy's local CA, which your\nOS doesn't trust. Click through on a private box. With a public domain + Let's\nEncrypt (below), the cert is trusted and there's no warning.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"going-to-production\">Going to production<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#going-to-production\" class=\"hash-link\" aria-label=\"Direct link to Going to production\" title=\"Direct link to Going to production\" translate=\"no\">​</a></h2>\n<p>With a real domain, swap two lines in the Caddyfile — point it at your domain and\ndrop <code>tls internal</code>:</p>\n<div class=\"language-diff codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-diff codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">- openclaw.local {</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">-     tls internal</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">+ openclaw.example.com {</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">      reverse_proxy openclaw-gateway:18789</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  }</span><br></div></code></pre></div></div>\n<p>Then point the domain's DNS <strong>A record</strong> at the VM's public IP and open <strong>80 + 443</strong>\nto the internet. Caddy automatically provisions and renews a <strong>Let's Encrypt</strong>\ncertificate — trusted, no warning, no <code>--resolve</code> or <code>/etc/hosts</code> needed. Update\n<code>allowedOrigins</code> to <code>https://openclaw.example.com</code>.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"security-notes\">Security notes<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#security-notes\" class=\"hash-link\" aria-label=\"Direct link to Security notes\" title=\"Direct link to Security notes\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><strong>Single entry point</strong> — only Caddy's <code>:443</code> is exposed; the gateway has no host\nports (Step 8).</li>\n<li class=\"\"><strong>Device pairing</strong> — every browser needs one-time approval (<code>openclaw devices</code>).</li>\n<li class=\"\"><strong>Origin allowlist</strong> — the Control UI only accepts <code>https://openclaw.local</code>.</li>\n<li class=\"\"><strong>Rotate secrets</strong> — regenerate <code>OPENCLAW_GATEWAY_TOKEN</code> if it has ever been\nshown (screenshots, screen-shares).</li>\n</ul>\n<hr>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Real HTTPS + auth</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>You've finished <strong>Part 2</strong> of the <strong>Self-hosting OpenClaw</strong> series. Next:</p>\n<ul>\n<li class=\"\"><strong>Part 3 — <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/\">Add a Telegram channel</a></strong> <em>(next)</em> — talk to your now-secured agent from your phone.</li>\n<li class=\"\"><strong>Part 4 — <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/\">Make OpenClaw private with a NetBird mesh VPN</a></strong> — put it on a private mesh and close the public ports.</li>\n</ul>\n<p>Companion files (<code>Caddyfile</code>, <code>docker-compose.override.yml</code>) live in the WiLine\nmanifests repo <em>(link coming with the repo)</em>.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"teardown\">Teardown<a href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/#teardown\" class=\"hash-link\" aria-label=\"Direct link to Teardown\" title=\"Direct link to Teardown\" translate=\"no\">​</a></h2>\n<p>To remove just Caddy and reopen the direct ports, delete <code>docker-compose.override.yml</code>\nand <code>Caddyfile</code>, then <code>docker compose up -d</code>. To stop everything:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose down</span><br></div></code></pre></div></div>",
            "url": "https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/",
            "title": "Secure OpenClaw with a Caddy reverse proxy + HTTPS",
            "summary": "Put Caddy in front of OpenClaw for real HTTPS and device-paired auth, then close the gateway's ports so the proxy is the only way in — gotchas and all.",
            "date_modified": "2026-06-23T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "self-hosting",
                "docker",
                "openclaw",
                "caddy",
                "https",
                "security"
            ]
        },
        {
            "id": "https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://development-wec.wiline.com/docs/assets/images/openclaw-wordmark-003352a1a7f02afc3bd877ae6f2dc175.png\" alt=\"OpenClaw\"><span class=\"tutorialHero__plus\">+</span><svg viewBox=\"0 0 24 24\" fill=\"#2496ED\" aria-label=\"Docker\" class=\"tutorialHero__docker\"><path d=\"M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z\"></path></svg><span class=\"tutorialHero__plus\">+</span><svg viewBox=\"0 0 24 24\" fill=\"#0a0a0a\" aria-label=\"OpenAI\" class=\"tutorialHero__openai\"><path d=\"M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997z\"></path></svg></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 6 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->6</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->6<!-- --> earned</span></div><div class=\"skillTracker__series\">Self-hosting OpenClaw</div><ul class=\"skillTracker__steps\"><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"current\">1</span><span class=\"skillTracker__skill\" data-state=\"current\">Deploy your own AI assistant</span></span></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Real HTTPS + auth</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Chat from Telegram</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">4</span><span class=\"skillTracker__skill\" data-state=\"locked\">Private mesh access</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/run-openclaw-on-wec-models/\"><span class=\"skillTracker__dot\" data-state=\"locked\">5</span><span class=\"skillTracker__skill\" data-state=\"locked\">Run it on WEC models</span></a></li><li><a class=\"skillTracker__step\" href=\"https://development-wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Chat from WhatsApp</span></a></li></ul><div class=\"skillTracker__footer\">Finish this tutorial, then mark it complete below to earn the skill.</div></div></div>\n<p>Your AI assistant doesn't have to live in someone else's cloud.</p>\n<p>Deploy a self-hosted <a href=\"https://openclaw.ai/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">OpenClaw</a> AI agent on a WEC Instance with\nDocker Compose — from spinning up the VM to an agent that actually answers, using\nyour own model API key. Every command, version, and error below was captured\nfrom a real deployment on a WEC Instance.</p>\n<!-- -->\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>Reproducibility</div><div class=\"admonitionContent_BuS1\"><p>You bring your own model API key (OpenAI in this guide); WiLine handles the\nhosting. This was reproduced on a WEC Instance — <strong>Ubuntu 22.04.5 LTS, 8 vCPU,\n15 GiB RAM, 25 GB free disk</strong>. OpenClaw version <strong>2026.6.8</strong>, image\n<code>ghcr.io/openclaw/openclaw:latest</code>.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-youll-build\">What you'll build<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#what-youll-build\" class=\"hash-link\" aria-label=\"Direct link to What you'll build\" title=\"Direct link to What you'll build\" translate=\"no\">​</a></h2>\n<p>A single OpenClaw <strong>gateway</strong> container, configured with your model provider,\nreachable through its web Control UI and CLI.</p>\n<!-- -->\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"prerequisites\">Prerequisites<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">A WiLine Edge Cloud account</li>\n<li class=\"\">A model API key — this guide uses <strong>OpenAI</strong>; Anthropic and local models work too</li>\n<li class=\"\">An SSH key pair on your machine (<code>~/.ssh/id_ed25519</code> or similar)</li>\n<li class=\"\">About 20 minutes</li>\n</ul>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-1--provision-the-vm-on-wiline\">Step 1 — Provision the VM on WiLine<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#step-1--provision-the-vm-on-wiline\" class=\"hash-link\" aria-label=\"Direct link to Step 1 — Provision the VM on WiLine\" title=\"Direct link to Step 1 — Provision the VM on WiLine\" translate=\"no\">​</a></h2>\n<p>Spin up a WiLine VM that meets these requirements:</p>\n<table><thead><tr><th>Requirement</th><th>Value</th></tr></thead><tbody><tr><td>OS</td><td>Ubuntu 22.04 LTS (or 24.04 LTS)</td></tr><tr><td>Compute</td><td>≥ 2 vCPU / 4 GB RAM — OpenClaw needs ≥ 2 GB; the first run can be OOM-killed (exit 137) on 1 GB hosts</td></tr><tr><td>Disk</td><td>30 GB NVMe — 10 GB fills up fast once Docker images and logs land</td></tr><tr><td>Access</td><td>Your <strong>SSH public key</strong> added at deploy time, so you can log in without a password</td></tr></tbody></table>\n<p>The portal walkthrough is already documented — follow these and come back:</p>\n<ul>\n<li class=\"\"><strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/compute/instances/compute_instance/\">Deploy a Virtual Machine →</a></strong></li>\n<li class=\"\"><strong><a class=\"\" href=\"https://development-wec.wiline.com/docs/cloud_portal/platform/compute/ssh_keys/\">Configure an SSH Key →</a></strong></li>\n</ul>\n<p>Once it's running, SSH in:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">ssh</span><span class=\"token plain\"> ubuntu@</span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">your-vm-ip</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><br></div></code></pre></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-2--verify-the-box\">Step 2 — Verify the box<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#step-2--verify-the-box\" class=\"hash-link\" aria-label=\"Direct link to Step 2 — Verify the box\" title=\"Direct link to Step 2 — Verify the box\" translate=\"no\">​</a></h2>\n<p>Before installing anything, confirm what you're working with — the OS version and\nthe resources available. Note these numbers; they're useful when you later compare\nperformance or open a support ticket.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">lsb_release </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-a</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">nproc</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">free</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-h</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">df</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-h</span><span class=\"token plain\"> /</span><br></div></code></pre></div></div>\n<p>On our WEC Instance: <code>Ubuntu 22.04.5 LTS</code>, <code>8</code> vCPU, ~<code>15 GiB</code> RAM, <code>25 GB</code> free disk.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Terminal showing OS version, CPU, RAM and disk\" src=\"https://development-wec.wiline.com/docs/assets/images/step2-box-specs-c821280774bf4f5ec9205092f6646004.png\" width=\"584\" height=\"316\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-3--install-docker--compose\">Step 3 — Install Docker + Compose<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#step-3--install-docker--compose\" class=\"hash-link\" aria-label=\"Direct link to Step 3 — Install Docker + Compose\" title=\"Direct link to Step 3 — Install Docker + Compose\" translate=\"no\">​</a></h2>\n<p>Check whether Docker is already present:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--version</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose version</span><br></div></code></pre></div></div>\n<p>If it's not installed, follow Docker's official guide for your distro —\n<strong><a href=\"https://docs.docker.com/engine/install/ubuntu/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Install Docker Engine on Ubuntu</a></strong>.\nThe quickest path is Docker's\n<a href=\"https://docs.docker.com/engine/install/#install-using-the-convenience-script\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">convenience script</a>,\nfollowed by adding your user to the <code>docker</code> group so you can run it without\n<code>sudo</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-fsSL</span><span class=\"token plain\"> https://get.docker.com </span><span class=\"token operator\" style=\"color:#393A34\">|</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">sh</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">usermod</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-aG</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> </span><span class=\"token environment constant\" style=\"color:#36acaa\">$USER</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">newgrp </span><span class=\"token function\" style=\"color:#d73a49\">docker</span><br></div></code></pre></div></div>\n<p>You need <strong>Docker Compose v2</strong> (the <code>docker compose</code> subcommand, not the legacy\n<code>docker-compose</code> binary).</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>note</div><div class=\"admonitionContent_BuS1\"><p>If Docker is already present, the <code>get.docker.com</code> script detects it and warns\nrather than reinstalling — don't force it on a box with running containers. On a\nclean VM it installs from scratch. This deploy ran on a host where Docker\n<strong>29.1.3 / Compose v5.0.2</strong> was already installed.</p></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker --version and docker compose version output\" src=\"data:image/png;base64,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\" width=\"582\" height=\"234\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-4--get-the-compose-file\">Step 4 — Get the Compose file<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#step-4--get-the-compose-file\" class=\"hash-link\" aria-label=\"Direct link to Step 4 — Get the Compose file\" title=\"Direct link to Step 4 — Get the Compose file\" translate=\"no\">​</a></h2>\n<p>Create a project directory and pull OpenClaw's official <code>docker-compose.yml</code>:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> openclaw </span><span class=\"token operator\" style=\"color:#393A34\">&amp;&amp;</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">cd</span><span class=\"token plain\"> openclaw</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-fsSL</span><span class=\"token plain\"> https://raw.githubusercontent.com/openclaw/openclaw/main/docker-compose.yml </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-o</span><span class=\"token plain\"> docker-compose.yml</span><br></div></code></pre></div></div>\n<p>The file defines two services — <code>openclaw-gateway</code> (the long-running agent\ngateway) and <code>openclaw-cli</code> (a one-shot CLI that shares the gateway's network).\nBy default it <strong>builds from source</strong>; we'll point it at the prebuilt image\ninstead.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-5--configure-env\">Step 5 — Configure <code>.env</code><a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#step-5--configure-env\" class=\"hash-link\" aria-label=\"Direct link to step-5--configure-env\" title=\"Direct link to step-5--configure-env\" translate=\"no\">​</a></h2>\n<p>Create a <code>.env</code> next to the compose file. Pin the prebuilt image and set a\ndashboard token:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token assign-left variable\" style=\"color:#36acaa\">OPENCLAW_IMAGE</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token plain\">ghcr.io/openclaw/openclaw:latest</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token assign-left variable\" style=\"color:#36acaa\">OPENCLAW_GATEWAY_TOKEN</span><span class=\"token operator\" style=\"color:#393A34\">=</span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">paste output of: openssl rand </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-hex</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">3</span><span class=\"token operator file-descriptor important\" style=\"color:#393A34\">2</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Keep <code>.env</code> out of git</div><div class=\"admonitionContent_BuS1\"><p>It holds your gateway token (and any keys). Add it to <code>.gitignore</code>. Commit a\n<code>.env.example</code> with placeholder values instead.</p></div></div>\n<p>Pull the image:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose pull</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker compose pull completing with the Pulled line\" src=\"data:image/png;base64,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\" width=\"583\" height=\"176\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-6--pre-create-the-config-directory\">Step 6 — Pre-create the config directory<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#step-6--pre-create-the-config-directory\" class=\"hash-link\" aria-label=\"Direct link to Step 6 — Pre-create the config directory\" title=\"Direct link to Step 6 — Pre-create the config directory\" translate=\"no\">​</a></h2>\n<p>OpenClaw bind-mounts <code>~/.openclaw</code> into the container. Create it <strong>as your own\nuser first</strong> — otherwise Docker creates it as <code>root</code> and the container (which\nruns as user <code>node</code>, uid 1000) can't write to it:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">mkdir</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-p</span><span class=\"token plain\"> ~/.openclaw ~/.openclaw-auth-profile-secrets</span><br></div></code></pre></div></div>\n<p>If you skip this, you'll hit a permission error on first run — see\n<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#troubleshooting-real-errors\" class=\"\">Troubleshooting</a>.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-7--onboard\">Step 7 — Onboard<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#step-7--onboard\" class=\"hash-link\" aria-label=\"Direct link to Step 7 — Onboard\" title=\"Direct link to Step 7 — Onboard\" translate=\"no\">​</a></h2>\n<p>This is where you bring your own model — onboarding prompts for your provider and\nAPI key. This guide uses <strong>OpenAI</strong>, but Anthropic and local models work the same way.</p>\n<p>Run OpenClaw's onboarding through the gateway container. It's interactive — it\nasks for your provider and API key and writes the config:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> --no-deps </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--entrypoint</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">node</span><span class=\"token plain\"> openclaw-gateway </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  dist/index.js onboard </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--mode</span><span class=\"token plain\"> </span><span class=\"token builtin class-name\">local</span><span class=\"token plain\"> --no-install-daemon</span><br></div></code></pre></div></div>\n<p>Walk through the prompts:</p>\n<ul>\n<li class=\"\"><strong>Continue</strong> past the personal-use security notice → <strong>Yes</strong></li>\n<li class=\"\"><strong>Setup mode</strong> → <strong>QuickStart</strong></li>\n<li class=\"\"><strong>Provider</strong> → <strong>OpenAI</strong> (or your provider), then paste your <strong>API key</strong></li>\n<li class=\"\"><strong>Default model</strong> → keep the suggested one (<code>openai/gpt-5.5</code>)</li>\n<li class=\"\"><strong>Channel</strong> → search <code>skip</code> → <strong>Skip for now</strong> (messaging channels are a later\nguide)</li>\n<li class=\"\"><strong>Skills / Hooks</strong> → <strong>No / Skip for now</strong> (configure later with\n<code>openclaw configure</code>)</li>\n<li class=\"\"><strong>Hatch your agent</strong> → <strong>Hatch later</strong> (we start the gateway as a service next)</li>\n</ul>\n<p>It ends with <code>Onboarding complete.</code></p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Onboarding — selecting the OpenAI provider\" src=\"data:image/png;base64,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\" width=\"626\" height=\"391\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"Onboarding — &amp;quot;Model configured: openai/gpt-5.5&amp;quot;\" src=\"data:image/png;base64,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\" width=\"627\" height=\"392\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-8--apply-gateway-config-and-start\">Step 8 — Apply gateway config and start<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#step-8--apply-gateway-config-and-start\" class=\"hash-link\" aria-label=\"Direct link to Step 8 — Apply gateway config and start\" title=\"Direct link to Step 8 — Apply gateway config and start\" translate=\"no\">​</a></h2>\n<p>Onboarding wrote your provider and model. Now set the gateway's runtime options —\nrun mode, network bind, and the Control UI origins it will accept — then start it\nas a background service.</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> --no-deps </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--entrypoint</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">node</span><span class=\"token plain\"> openclaw-gateway </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  dist/index.js config </span><span class=\"token builtin class-name\">set</span><span class=\"token plain\"> --batch-json </span><span class=\"token string\" style=\"color:#e3116c\">'[{\"path\":\"gateway.mode\",\"value\":\"local\"},{\"path\":\"gateway.bind\",\"value\":\"lan\"},{\"path\":\"gateway.controlUi.allowedOrigins\",\"value\":[\"http://localhost:18789\",\"http://127.0.0.1:18789\"]}]'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose up </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-d</span><span class=\"token plain\"> openclaw-gateway</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker compose up -d creating the gateway container\" src=\"data:image/png;base64,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\" width=\"629\" height=\"169\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"step-9--sanity-check\">Step 9 — Sanity check<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#step-9--sanity-check\" class=\"hash-link\" aria-label=\"Direct link to Step 9 — Sanity check\" title=\"Direct link to Step 9 — Sanity check\" translate=\"no\">​</a></h2>\n<p>Container health:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose </span><span class=\"token function\" style=\"color:#d73a49\">ps</span><br></div></code></pre></div></div>\n<p>You want <code>Up ... (healthy)</code>. Then hit the health endpoint:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">curl</span><span class=\"token plain\"> http://127.0.0.1:18789/healthz</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># {\"ok\":true,\"status\":\"live\"}</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"docker compose ps healthy and the healthz JSON response\" src=\"https://development-wec.wiline.com/docs/assets/images/step9-health-2a25a28bc6b53dd20c385259373d72a5.png\" width=\"626\" height=\"217\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>Confirm your provider key is usable:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli models status</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># openai ... api_key=1 ... status=usable</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-warning admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>Redact before screenshotting</div><div class=\"admonitionContent_BuS1\"><p><code>models status</code> prints a masked key prefix (<code>sk-proj-…</code>). Crop or blur it before\npublishing the screenshot.</p></div></div>\n<p>Now the real end-to-end test. The <code>agent</code> command needs a target, so list agents\nfirst, then message the default one (<code>main</code>):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli agents list</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># - main (default)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli agent </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--agent</span><span class=\"token plain\"> main </span><span class=\"token punctuation\" style=\"color:#393A34\">\\</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">  </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--message</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"Reply with exactly one word: working\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># working</span><br></div></code></pre></div></div>\n<p>If it replies <code>working</code>, the full chain — gateway → OpenAI → response — is live.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The agent replying &amp;quot;working&amp;quot; — the proof shot\" src=\"https://development-wec.wiline.com/docs/assets/images/step9-agent-working-91514c6b42235714c1348d5ce9f6e9b9.png\" width=\"628\" height=\"391\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<p>The Control UI is at <code>http://&lt;your-instance-ip&gt;:18789/</code>; paste your\n<code>OPENCLAW_GATEWAY_TOKEN</code> into Settings to log in.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Opening it from another machine</div><div class=\"admonitionContent_BuS1\"><p>Browsers block the Control UI over plain HTTP at a remote IP (\"Secure browser\ncontext required\"). Reach it over an SSH tunnel so it loads as <code>127.0.0.1</code> — see\n<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#3-control-ui-secure-browser-context-required\" class=\"\">Troubleshooting #3</a>.</p></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"OpenClaw Control UI in the browser, logged in\" src=\"https://development-wec.wiline.com/docs/assets/images/step9-control-ui-fe1df8b0971ad1a8192ff40d2ea6856e.png\" width=\"1100\" height=\"567\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"troubleshooting-real-errors\">Troubleshooting (real errors)<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#troubleshooting-real-errors\" class=\"hash-link\" aria-label=\"Direct link to Troubleshooting (real errors)\" title=\"Direct link to Troubleshooting (real errors)\" translate=\"no\">​</a></h2>\n<p>These are errors we actually hit during this deploy — not hypotheticals.</p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"1-eacces-permission-denied-mkdir-homenodeopenclawstate\">1. <code>EACCES: permission denied, mkdir '/home/node/.openclaw/state'</code><a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#1-eacces-permission-denied-mkdir-homenodeopenclawstate\" class=\"hash-link\" aria-label=\"Direct link to 1-eacces-permission-denied-mkdir-homenodeopenclawstate\" title=\"Direct link to 1-eacces-permission-denied-mkdir-homenodeopenclawstate\" translate=\"no\">​</a></h3>\n<p>Hit on the first onboarding run when <code>~/.openclaw</code> didn't pre-exist:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[openclaw] The CLI command failed.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">[openclaw] Reason: Failed to open the plugin state database.</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">| EACCES: permission denied, mkdir '/home/node/.openclaw/state' | EACCES</span><br></div></code></pre></div></div>\n<p><strong>Cause:</strong> Docker auto-created the bind-mount source <code>~/.openclaw</code> owned by\n<code>root</code> (<code>0:0</code>). The container runs as user <code>node</code> (uid 1000), so it can't write\ninside that root-owned directory.</p>\n<p><strong>Fix:</strong> give the directory to uid 1000 (which is also your <code>ubuntu</code> user):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">sudo</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:#d73a49\">chown</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-R</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">1000</span><span class=\"token plain\">:1000 ~/.openclaw</span><br></div></code></pre></div></div>\n<p>Pre-creating the directory yourself (Step 6) avoids this entirely.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"EACCES permission error in the terminal\" src=\"https://development-wec.wiline.com/docs/assets/images/error1-eacces-fbbf355224203ecfccd68552fcf0bcb3.png\" width=\"968\" height=\"246\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"2-no-target-session-selected\">2. <code>No target session selected</code><a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#2-no-target-session-selected\" class=\"hash-link\" aria-label=\"Direct link to 2-no-target-session-selected\" title=\"Direct link to 2-no-target-session-selected\" translate=\"no\">​</a></h3>\n<p>The first time we ran <code>agent</code> without specifying who to talk to:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Error: No target session selected. Use --agent &lt;id&gt;, --session-key &lt;key&gt;,</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">--session-id &lt;id&gt;, or --to &lt;E.164&gt;. Run openclaw agents list to see agents.</span><br></div></code></pre></div></div>\n<p><strong>Cause:</strong> <code>openclaw agent</code> runs one turn against a specific agent/session; with\nno channel configured there's no implicit target.</p>\n<p><strong>Fix:</strong> list agents and pass the id explicitly:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli agents list          </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># shows: main (default)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose run </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rm</span><span class=\"token plain\"> openclaw-cli agent </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--agent</span><span class=\"token plain\"> main </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--message</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:#e3116c\">\"...\"</span><br></div></code></pre></div></div>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The &amp;quot;No target session selected&amp;quot; error\" src=\"https://development-wec.wiline.com/docs/assets/images/error2-no-session-c405ca89ca87bd411080165c514046e5.png\" width=\"839\" height=\"241\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<h3 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"3-control-ui-secure-browser-context-required\">3. Control UI: \"Secure browser context required\"<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#3-control-ui-secure-browser-context-required\" class=\"hash-link\" aria-label=\"Direct link to 3. Control UI: &quot;Secure browser context required&quot;\" title=\"Direct link to 3. Control UI: &quot;Secure browser context required&quot;\" translate=\"no\">​</a></h3>\n<p>Opening the Control UI from another machine (e.g. macOS) at the VM's IP over\nplain HTTP, the page refuses to connect:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">Secure browser context required</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">This page is running over plain HTTP, so the browser cannot create the</span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\">device identity the Gateway expects.</span><br></div></code></pre></div></div>\n<p><strong>Cause:</strong> browsers only expose the crypto APIs OpenClaw needs in a <em>secure\ncontext</em> — HTTPS, or <code>localhost</code>/<code>127.0.0.1</code>. A remote <code>http://&lt;ip&gt;:18789</code> is\nneither, so it's blocked in the <strong>browser</strong>, regardless of the server-side\n<code>gateway.controlUi.allowInsecureAuth</code> flag.</p>\n<p><strong>Fix:</strong> tunnel the port over SSH so the browser talks to <code>127.0.0.1</code> (a secure\ncontext). On your local machine:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">ssh</span><span class=\"token plain\"> </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-L</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:#36acaa\">18789</span><span class=\"token plain\">:127.0.0.1:18789 ubuntu@</span><span class=\"token operator\" style=\"color:#393A34\">&lt;</span><span class=\"token plain\">your-vm-ip</span><span class=\"token operator\" style=\"color:#393A34\">&gt;</span><br></div></code></pre></div></div>\n<p>Then open <code>http://127.0.0.1:18789/#token=&lt;your-token&gt;</code> locally. The proper\nlong-term fix is HTTPS via a reverse proxy — the next guide.</p>\n<p><span class=\"zoomImage__wrap\"><img alt=\"The red &amp;quot;Secure browser context required&amp;quot; message in the Control UI\" src=\"https://development-wec.wiline.com/docs/assets/images/error3-secure-context-3e5792a56fae6a2ea2a8f9e0f0aa4c3f.png\" width=\"658\" height=\"895\" class=\"zoomImage \" loading=\"lazy\"><span class=\"zoomImage__badge\" aria-hidden=\"true\"><svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><circle cx=\"11\" cy=\"11\" r=\"7\"></circle><path d=\"M21 21l-4.3-4.3\"></path><path d=\"M11 8v6M8 11h6\"></path></svg></span></span></p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"security-notes\">Security notes<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#security-notes\" class=\"hash-link\" aria-label=\"Direct link to Security notes\" title=\"Direct link to Security notes\" translate=\"no\">​</a></h2>\n<p>On startup the gateway logged warnings worth acting on before any public\nexposure:</p>\n<ul>\n<li class=\"\"><strong>Binding to a non-loopback address</strong> — the gateway listens on the LAN. Don't\nexpose port 18789 to the public internet without auth in front of it.</li>\n<li class=\"\"><strong><code>gateway.controlUi.allowInsecureAuth=true</code></strong> — flagged dangerous. Run\n<code>docker compose run --rm openclaw-cli security audit</code>.</li>\n<li class=\"\"><strong><code>plugins.allow</code> is empty</strong> — non-bundled plugins (e.g. <code>codex</code>) may\nauto-load. Set an explicit allowlist of trusted plugin ids.</li>\n</ul>\n<p>Hardening this behind a reverse proxy with HTTPS is the subject of the next\nguide.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Skill unlocked 🏅</div><div class=\"admonitionContent_BuS1\"><p>You can now <strong>deploy and operate your own AI agent</strong> on a box you control.</p></div></div>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-youve-accomplished\">What you've accomplished<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#what-youve-accomplished\" class=\"hash-link\" aria-label=\"Direct link to What you've accomplished\" title=\"Direct link to What you've accomplished\" translate=\"no\">​</a></h2>\n<p>You went from a bare WEC Instance to a working, self-hosted AI agent:</p>\n<ul>\n<li class=\"\">Provisioned and verified an Ubuntu VM on WEC</li>\n<li class=\"\">Stood up the OpenClaw gateway with Docker Compose and the official prebuilt image</li>\n<li class=\"\">Wired in your own model provider (OpenAI) and confirmed it end-to-end — the agent\nreplied over the CLI and the Control UI</li>\n<li class=\"\">Hit (and fixed) the real permission, session, and secure-context issues along the\nway</li>\n</ul>\n<p>The agent is yours: your key, your box, your data. From here it's about making it\nsecure, reachable, and useful.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-can-you-do-with-it\">What can you do with it?<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#what-can-you-do-with-it\" class=\"hash-link\" aria-label=\"Direct link to What can you do with it?\" title=\"Direct link to What can you do with it?\" translate=\"no\">​</a></h2>\n<p>OpenClaw isn't a chatbot — it's an <strong>autonomous agent that can act on your box</strong>. Now\nthat it's running, here's what it unlocks (some features need extra configuration, and\nwe cover the big ones later in this series):</p>\n<ul>\n<li class=\"\"><strong>Run real work on the machine.</strong> It can execute shell commands and read/write files\n— <em>\"check what's using the disk,\"</em> <em>\"tail the logs and summarize the errors\"</em> — with\nconfigurable sandboxing for how much you let it touch.</li>\n<li class=\"\"><strong>Talk to it from your phone.</strong> OpenClaw connects to <strong>WhatsApp, Telegram, Discord,\nSlack, Signal, and iMessage</strong>, so you delegate tasks conversationally without SSH.\n<em>(We wire up Telegram in the next guide.)</em></li>\n<li class=\"\"><strong>Browse and act on the web.</strong> Built-in browser automation lets it navigate sites,\nfill forms, and extract data autonomously.</li>\n<li class=\"\"><strong>Write and run code.</strong> It integrates with <strong>Claude Code</strong> for autonomous coding —\nwriting and modifying code, running tests, and even opening PRs.</li>\n<li class=\"\"><strong>Work in the background, proactively.</strong> It runs 24/7, can execute background tasks,\nand does \"heartbeat\" check-ins instead of only reacting when you message it.</li>\n<li class=\"\"><strong>Grow with you.</strong> A community skill library (<strong>ClawHub</strong>), self-written tools, and\nmulti-agent orchestration mean it gets more capable over time — backed by persistent\nmemory that carries context across conversations.</li>\n</ul>\n<p>Typical real uses people run: inbox and calendar management, document processing,\ninfrastructure tasks on the box, content pipelines, and autonomous code testing.</p>\n<hr>\n<div class=\"skillComplete\"><span class=\"skillComplete__medal\" aria-hidden=\"true\">🎯</span><div class=\"skillComplete__text\"><div class=\"skillComplete__title\">Finished this tutorial?</div><div class=\"skillComplete__sub\">Mark it complete to earn <strong>Deploy your own AI assistant</strong> on your skill path.</div></div><button type=\"button\" class=\"skillComplete__btn\">Mark as complete ✓</button></div>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"whats-next\">What's next<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>This is <strong>Part 1</strong> of the <strong>Self-hosting OpenClaw</strong> series on running production-grade\nAI infrastructure on a WEC Instance. Coming up:</p>\n<ul>\n<li class=\"\"><strong>Part 2 — <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/\">Secure OpenClaw with a Caddy reverse proxy + HTTPS</a></strong> <em>(next)</em> — put HTTPS in front of the gateway so you can drop the <code>allowInsecureAuth</code> workaround and reach the Control UI from anywhere, safely.</li>\n<li class=\"\"><strong>Part 3 — <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/\">Add a Telegram channel</a></strong> — talk to your agent from your phone.</li>\n<li class=\"\"><strong>Part 4 — <a class=\"\" href=\"https://development-wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/\">Make OpenClaw private with a NetBird mesh VPN</a></strong> — put it on a private mesh and close the public ports.</li>\n</ul>\n<p>Companion files for this guide — <code>docker-compose.yml</code> and <code>.env.example</code> — live in\nthe WiLine manifests repo <em>(link coming with the repo)</em>.</p>\n<hr>\n<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"teardown\">Teardown<a href=\"https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/#teardown\" class=\"hash-link\" aria-label=\"Direct link to Teardown\" title=\"Direct link to Teardown\" translate=\"no\">​</a></h2>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#393A34;--prism-background-color:#f6f8fa\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#393A34;background-color:#f6f8fa\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose down              </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># stop + remove containers</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#393A34\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:#d73a49\">docker</span><span class=\"token plain\"> compose down </span><span class=\"token parameter variable\" style=\"color:#36acaa\">--rmi</span><span class=\"token plain\"> all </span><span class=\"token parameter variable\" style=\"color:#36acaa\">-v</span><span class=\"token plain\"> </span><span class=\"token comment\" style=\"color:#999988;font-style:italic\"># also remove the image and volumes</span><br></div></code></pre></div></div>\n<p>To remove config/state on the host:\n<code>rm -rf ~/.openclaw ~/.openclaw-auth-profile-secrets</code>. To stop billing entirely,\ndelete the VM from the WiLine portal.</p>",
            "url": "https://development-wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/",
            "title": "Deploy OpenClaw on a WEC Instance via Docker Compose",
            "summary": "From a fresh WEC Instance to a self-hosted OpenClaw agent that actually answers — Docker Compose, your own model key, and every real error and fix from a live deploy.",
            "date_modified": "2026-06-18T00:00:00.000Z",
            "author": {
                "name": "Rafael Fernandes",
                "url": "https://www.linkedin.com/in/rafaelmacariofernandes/"
            },
            "tags": [
                "ai",
                "self-hosting",
                "docker",
                "openclaw",
                "vps"
            ]
        }
    ]
}