{
    "version": "https://jsonfeed.org/version/1",
    "title": "WiLine Edge Cloud — AI Tutorials",
    "home_page_url": "https://wec.wiline.com/docs/tutorials/",
    "description": "Tested, reproducible tutorials for self-hosting AI infrastructure on WiLine.",
    "items": [
        {
            "id": "https://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://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 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\">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><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"locked\">2</span><span class=\"skillTracker__skill\" data-state=\"locked\">Route work to the right model</span></span></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"locked\">3</span><span class=\"skillTracker__skill\" data-state=\"locked\">Mask PII at the gateway</span></span></li><li><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Budgets and real cost</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>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<h2 class=\"anchor anchorTargetStickyNavbar_Vzrq\" id=\"what-were-putting-on-the-box\">What we're putting on the box<a href=\"https://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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>Token counts are real. The dollar figures are not — not for you, anyway. These are\nself-hosted models on your own infrastructure; the gateway is pricing them off a\nbuilt-in table of public API rates. The number is precise and confidently wrong,\nwhich is worse than blank. Fixing that means declaring your own cost per token, and\nthat's the subject of the fourth post in this series.</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://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://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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/harden-container-privilege-non-root/\">Root by default: hardening container privilege</a></li>\n</ul>",
            "url": "https://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://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><img class=\"tutorialHero__langfuse\" src=\"data:image/webp;base64,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\" alt=\"Langfuse\"></div>\n<div class=\"skillTracker\" tabindex=\"0\" aria-label=\"Skill path: 0 of 3 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->3</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->3<!-- --> earned</span></div><div class=\"skillTracker__series\">Hardening self-hosted AI infra</div><ul class=\"skillTracker__steps\"><li><a class=\"skillTracker__step\" href=\"https://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><span class=\"skillTracker__step\"><span class=\"skillTracker__dot\" data-state=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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 3 skills earned\"><div class=\"skillTracker__ring\" style=\"--skillTracker-pct:0%\"><span class=\"skillTracker__ringLabel\">0<!-- -->/<!-- -->3</span></div><div class=\"skillTracker__panel\"><div class=\"skillTracker__header\"><span class=\"skillTracker__title\">🎯 Skill path</span><span class=\"skillTracker__count\">0<!-- -->/<!-- -->3<!-- --> 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://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=\"locked\">🏆</span><span class=\"skillTracker__skill\" data-state=\"locked\">Identity in front of every port</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>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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/telegram-on-hermes/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/web-search-wiline-inference/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/component-tracing-agent-tool-calls-wiline/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__langfuse\" src=\"data:image/webp;base64,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\" alt=\"Langfuse\"><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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/whatsapp-ai-assistant-evolution-api/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/add-whatsapp-channel-openclaw/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://wec.wiline.com/docs/assets/images/openclaw-wordmark-003352a1a7f02afc3bd877ae6f2dc175.png\" alt=\"OpenClaw\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\">deploy</a>,\n<a class=\"\" href=\"https://wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/\">HTTPS</a>,\n<a class=\"\" href=\"https://wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/\">Telegram</a>,\n<a class=\"\" href=\"https://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://wec.wiline.com/docs/assets/images/promptfoo-avatar-31b32c657d72dac0a7edbd77c24ec717.jpg\" alt=\"Promptfoo\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__langfuse\" src=\"data:image/webp;base64,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\" alt=\"Langfuse\"></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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\">part 1</a>), make its output machine-reliable\n(<a class=\"\" href=\"https://wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/\">part 2</a>), generate real test data\n(<a class=\"\" href=\"https://wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/\">part 3</a>), and observe production\n(<a class=\"\" href=\"https://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/observe-production-langfuse-wiline/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__langfuse\" src=\"data:image/webp;base64,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\" alt=\"Langfuse\"><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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\">part 1</a>),\nmake its output machine-reliable (<a class=\"\" href=\"https://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/synthetic-eval-datasets-wiline-inference/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/\">part 1</a> and\n<a class=\"\" href=\"https://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/structured-output-evals-wiline-inference/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/eval-models-promptfoo-wiline-inference/",
            "content_html": "\n<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/self-host-hermes-agent/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://wec.wiline.com/docs/assets/images/hermesagent-c76bbe81610ae2e70b0950e0f96bec8a.png\" alt=\"Hermes Agent\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__brand\" src=\"https://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/netbird-mesh-vpn-openclaw/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://wec.wiline.com/docs/assets/images/openclaw-wordmark-003352a1a7f02afc3bd877ae6f2dc175.png\" alt=\"OpenClaw\"><span class=\"tutorialHero__plus\">+</span><img class=\"tutorialHero__docker\" src=\"https://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/add-telegram-channel-openclaw/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\">deployed OpenClaw</a> and\n<a class=\"\" href=\"https://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://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://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://wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/\">Part 1</a></strong> + <strong><a class=\"\" href=\"https://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/secure-openclaw-caddy-https/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://wec.wiline.com/docs/tutorials/deploy-openclaw-docker-compose/",
            "content_html": "<div class=\"tutorialHero\"><img class=\"tutorialHero__brand\" src=\"https://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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://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"
            ]
        }
    ]
}